Bläddra i källkod

[lua] SkeletonJson 4.0 port, clean up.

Nathan Sweet 4 år sedan
förälder
incheckning
3c0a43687b

+ 1611 - 1612
spine-lua/spine-lua/Animation.lua

@@ -1,1612 +1,1611 @@
--------------------------------------------------------------------------------
--- Spine Runtimes License Agreement
--- Last updated January 1, 2020. Replaces all prior versions.
---
--- Copyright (c) 2013-2020, Esoteric Software LLC
---
--- Integration of the Spine Runtimes into software or otherwise creating
--- derivative works of the Spine Runtimes is permitted under the terms and
--- conditions of Section 2 of the Spine Editor License Agreement:
--- http://esotericsoftware.com/spine-editor-license
---
--- Otherwise, it is permitted to integrate the Spine Runtimes into software
--- or otherwise create derivative works of the Spine Runtimes (collectively,
--- "Products"), provided that each user of the Products must obtain their own
--- Spine Editor license and redistribution of the Products in any form must
--- include this license and copyright notice.
---
--- THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
--- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
--- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
--- DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
--- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
--- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
--- BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
--- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
--- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
--- THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
-
--- FIXME
--- All the indexing in this file is zero based. We use zlen()
--- instead of the # operator. Initialization of number arrays
--- is performed via utils.newNumberArrayZero. This needs
--- to be rewritten using one-based indexing for better performance
-
-local utils = require "spine-lua.utils"
-local AttachmentType = require "spine-lua.attachments.AttachmentType"
-local math_floor = math.floor
-local math_abs = math.abs
-local math_signum = utils.signum
-
-local function zlen(array)
-	return #array + 1
-end
-
-local Animation = {}
-function Animation.new (name, timelines, duration)
-	if not timelines then error("timelines cannot be nil", 2) end
-
-	local self = {
-		name = name,
-		timelines = timelines,
-		timelineIds = {},
-		duration = duration
-	}
-	
-	for i,timeline in ipairs(self.timelines) do
-		self.timelineIds[timeline:getPropertyId()] = true
-	end
-
-	function self:hasTimeline(id)
-		return self.timelineIds[id] == true
-	end
-
-	function self:apply (skeleton, lastTime, time, loop, events, alpha, blend, direction)
-		if not skeleton then error("skeleton cannot be nil.", 2) end
-
-		if loop and duration > 0 then
-			time = time % self.duration
-			if lastTime > 0 then lastTime = lastTime % self.duration end
-		end
-
-		for i,timeline in ipairs(self.timelines) do
-			timeline:apply(skeleton, lastTime, time, events, alpha, blend, direction)
-		end
-	end
-
-	return self
-end
-
-local function binarySearch (values, target, step)
-	local low = 0
-	local high = math.floor(zlen(values) / step - 2)
-	if high == 0 then return step end
-	local current = math.floor(high / 2)
-	while true do
-		if values[(current + 1) * step] <= target then
-			low = current + 1
-		else
-			high = current
-		end
-		if low == high then return (low + 1) * step end
-		current = math.floor((low + high) / 2)
-	end
-end
-Animation.binarySearch = binarySearch
-
-local function binarySearch1 (values, target)
-	local low = 0
-	local high = math.floor(zlen(values)	- 2)
-	if high == 0 then return 1 end
-	local current = math.floor(high / 2)
-	while true do
-		if values[current + 1] <= target then
-			low = current + 1
-		else
-			high = current
-		end
-		if low == high then return low + 1 end
-		current = math.floor((low + high) / 2)
-	end
-end
-
-local function linearSearch (values, target, step)
-	local i = 0
-	local last = zlen(values) - step
-	while i <= last do
-		if (values[i] > target) then return i end
-		i = i + step
-	end
-	return -1
-end
-
-Animation.MixBlend = {
-	setup = 0,
-	first = 1,
-	replace = 2,
-	add = 3
-}
-local MixBlend = Animation.MixBlend
-
-Animation.MixDirection = {
-	_in = 0, out = 1
-}
-local MixDirection = Animation.MixDirection
-
-Animation.TimelineType = {
-	rotate = 0, translate = 1, scale = 2, shear = 3,
-	attachment = 4, color = 5, deform = 6,
-	event = 7, drawOrder = 8,
-	ikConstraint = 9, transformConstraint = 10,
-	pathConstraintPosition = 11, pathConstraintSpacing = 12, pathConstraintMix = 13,
-	twoColor = 14
-}
-local TimelineType = Animation.TimelineType
-local SHL_24 = 16777216
-local SHL_27 = 134217728
-
-Animation.CurveTimeline = {}
-function Animation.CurveTimeline.new (frameCount)
-	local LINEAR = 0
-	local STEPPED = 1
-	local BEZIER = 2
-	local BEZIER_SIZE = 10 * 2 - 1
-
-	local self = {
-		curves = utils.newNumberArrayZero((frameCount - 1) * BEZIER_SIZE) -- type, x, y, ...
-	}
-
-	function self:getFrameCount ()
-		return math.floor(zlen(self.curves) / BEZIER_SIZE) + 1
-	end
-
-	function self:setStepped (frameIndex)
-		self.curves[frameIndex * BEZIER_SIZE] = STEPPED
-	end
-
-	function self:getCurveType (frameIndex)
-		local index = frameIndex * BEZIER_SIZE
-		if index == zlen(self.curves) then return LINEAR end
-		local type = self.curves[index]
-		if type == LINEAR then return LINEAR end
-		if type == STEPPED then return STEPPED end
-		return BEZIER
-	end
-
-	function self:setCurve (frameIndex, cx1, cy1, cx2, cy2)
-			local tmpx = (-cx1 * 2 + cx2) * 0.03
-			local tmpy = (-cy1 * 2 + cy2) * 0.03
-			local dddfx = ((cx1 - cx2) * 3 + 1) * 0.006
-			local dddfy = ((cy1 - cy2) * 3 + 1) * 0.006
-			local ddfx = tmpx * 2 + dddfx
-			local ddfy = tmpy * 2 + dddfy
-			local dfx = cx1 * 0.3 + tmpx + dddfx * 0.16666667
-			local dfy = cy1 * 0.3 + tmpy + dddfy * 0.16666667
-
-			local i = frameIndex * BEZIER_SIZE
-			local curves = self.curves
-			curves[i] = BEZIER
-			i = i + 1
-
-			local x = dfx
-			local y = dfy
-			local n = i + BEZIER_SIZE - 1
-			while i < n do
-				curves[i] = x
-				curves[i + 1] = y
-				dfx = dfx + ddfx
-				dfy = dfy + ddfy
-				ddfx = ddfx + dddfx
-				ddfy = ddfy + dddfy
-				x = x + dfx
-				y = y + dfy
-				i = i + 2
-			end
-	end
-
-	function self:getCurvePercent (frameIndex, percent)
-		percent = utils.clamp(percent, 0, 1)
-		local curves = self.curves
-		local i = frameIndex * BEZIER_SIZE
-		local type = curves[i]
-		if type == LINEAR then return percent end
-		if type == STEPPED then return 0 end
-		i = i + 1
-		local x
-		local n = i + BEZIER_SIZE - 1
-		local start = i
-		while i < n do
-			x = curves[i]
-			if x >= percent then
-				local prevX, prevY
-				if i == start then
-					prevX = 0
-					prevY = 0
-				else
-					prevX = curves[i - 2]
-					prevY = curves[i - 1]
-				end
-				return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX)
-			end
-			i = i + 2
-		end
-		local y = curves[i - 1]
-		return y + (1 - y) * (percent - x) / (1 - x) -- Last point is 1,1.
-	end
-
-	return self
-end
-
-Animation.RotateTimeline = {}
-Animation.RotateTimeline.ENTRIES = 2
-Animation.RotateTimeline.PREV_TIME = -2
-Animation.RotateTimeline.PREV_ROTATION = -1
-Animation.RotateTimeline.ROTATION = 1
-function Animation.RotateTimeline.new (frameCount)
-	local ENTRIES = Animation.RotateTimeline.ENTRIES
-	local PREV_TIME = Animation.RotateTimeline.PREV_TIME
-	local PREV_ROTATION = Animation.RotateTimeline.PREV_ROTATION
-	local ROTATION = Animation.RotateTimeline.ROTATION
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.boneIndex = -1
-	self.frames = utils.newNumberArrayZero(frameCount * 2)
-	self.type = TimelineType.rotate
-
-	function self:getPropertyId ()
-		return TimelineType.rotate * SHL_24 + self.boneIndex
-	end
-
-	function self:setFrame (frameIndex, time, degrees)
-		frameIndex = frameIndex * 2
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + ROTATION] = degrees
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local bone = skeleton.bones[self.boneIndex]
-		if not bone.active then return end
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				bone.rotation = bone.data.rotation
-			elseif blend == MixBlend.first then
-				local r = bone.data.rotation - bone.rotation
-				bone.rotation = bone.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
-			end
-			return
-		end
-
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			local r = frames[zlen(frames) + PREV_ROTATION]
-			if blend == MixBlend.setup then
-				bone.rotation = bone.data.rotation + r * alpha
-			elseif blend == MixBlend.first or blend == MixBlend.replace then
-				r = r + bone.data.rotation - bone.rotation
-				r = r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360 -- Wrap within -180 and 180.
-				bone.rotation = bone.rotation + r * alpha;
-			elseif blend == MixBlend.add then
-				bone.rotation = bone.rotation + r * alpha;
-			end
-			return;
-		end
-
-		-- Interpolate between the last frame and the current frame.
-		local frame = binarySearch(frames, time, ENTRIES)
-		local prevRotation = frames[frame + PREV_ROTATION]
-		local frameTime = frames[frame]
-		local percent = self:getCurvePercent((math.floor(frame / 2)) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-		local r = frames[frame + ROTATION] - prevRotation
-		r = prevRotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * percent
-		if blend == MixBlend.setup then
-			bone.rotation = bone.data.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
-		elseif blend == MixBlend.first or blend == MixBlend.replace then
-			r = r + bone.data.rotation - bone.rotation;
-			bone.rotation = bone.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
-		elseif blend == MixBlend.add then
-			bone.rotation = bone.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
-		end
-	end
-
-	return self
-end
-
-Animation.TranslateTimeline = {}
-Animation.TranslateTimeline.ENTRIES = 3
-function Animation.TranslateTimeline.new (frameCount)
-	local ENTRIES = Animation.TranslateTimeline.ENTRIES
-	local PREV_TIME = -3
-	local PREV_X = -2
-	local PREV_Y = -1
-	local X = 1
-	local Y = 2
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.boneIndex = -1
-	self.type = TimelineType.translate
-
-	function self:getPropertyId ()
-		return TimelineType.translate * SHL_24 + self.boneIndex
-	end
-
-	function self:setFrame (frameIndex, time, x, y)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + X] = x
-		self.frames[frameIndex + Y] = y
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local bone = skeleton.bones[self.boneIndex]
-		if not bone.active then return end
-
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				bone.x = bone.data.x
-				bone.y = bone.data.y
-			elseif blend == MixBlend.first then
-				bone.x = bone.x + (bone.data.x - bone.x) * alpha
-				bone.y = bone.y + (bone.data.y - bone.y) * alpha
-			end
-			return
-		end
-
-		local x = 0
-		local y = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- // Time is after last frame.
-			x = frames[zlen(frames) + PREV_X];
-			y = frames[zlen(frames) + PREV_Y];
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			x = frames[frame + PREV_X]
-			y = frames[frame + PREV_Y]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			x = x + (frames[frame + X] - x) * percent
-			y = y + (frames[frame + Y] - y) * percent
-		end
-		if blend == MixBlend.setup then
-			bone.x = bone.data.x + x * alpha
-			bone.y = bone.data.y + y * alpha
-		elseif blend == MixBlend.first or blend == MixBlend.replace then
-			bone.x = bone.x + (bone.data.x + x - bone.x) * alpha
-			bone.y = bone.y + (bone.data.y + y - bone.y) * alpha
-		elseif blend == MixBlend.add then
-			bone.x = bone.x + x * alpha
-			bone.y = bone.y + y * alpha
-		end
-	end
-
-	return self
-end
-
-Animation.ScaleTimeline = {}
-Animation.ScaleTimeline.ENTRIES = Animation.TranslateTimeline.ENTRIES
-function Animation.ScaleTimeline.new (frameCount)
-	local ENTRIES = Animation.ScaleTimeline.ENTRIES
-	local PREV_TIME = -3
-	local PREV_X = -2
-	local PREV_Y = -1
-	local X = 1
-	local Y = 2
-
-	local self = Animation.TranslateTimeline.new(frameCount)
-	self.type = TimelineType.scale
-
-	function self:getPropertyId ()
-		return TimelineType.scale * SHL_24 + self.boneIndex
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local bone = skeleton.bones[self.boneIndex]
-		if not bone.active then return end
-
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				bone.scaleX = bone.data.scaleX
-				bone.scaleY = bone.data.scaleY
-			elseif blend == MixBlend.first then
-				bone.scaleX = bone.scaleX + (bone.data.scaleX - bone.scaleX) * alpha
-				bone.scaleY = bone.scaleY + (bone.data.scaleY - bone.scaleY) * alpha
-			end
-			return
-		end
-
-		local x = 0
-		local y = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			x = frames[zlen(frames) + PREV_X] * bone.data.scaleX
-			y = frames[zlen(frames) + PREV_Y] * bone.data.scaleY
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			x = frames[frame + PREV_X]
-			y = frames[frame + PREV_Y]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			x = (x + (frames[frame + X] - x) * percent) * bone.data.scaleX
-			y = (y + (frames[frame + Y] - y) * percent) * bone.data.scaleY
-		end
-		if alpha == 1 then
-			if blend == MixBlend.add then
-				bone.scaleX = bone.scaleX + x - bone.data.scaleX
-				bone.scaleY = bone.scaleY + y - bone.data.scaleY
-			else
-				bone.scaleX = x
-				bone.scaleY = y
-			end
-		else
-			local bx = 0
-			local by = 0
-			if direction == MixDirection.out then
-				if blend == MixBlend.setup then
-					bx = bone.data.scaleX
-					by = bone.data.scaleY
-					bone.scaleX = bx + (math_abs(x) * math_signum(bx) - bx) * alpha
-					bone.scaleY = by + (math_abs(y) * math_signum(by) - by) * alpha
-				elseif blend == MixBlend.first or blend == MixBlend.replace then
-					bx = bone.scaleX
-					by = bone.scaleY
-					bone.scaleX = bx + (math_abs(x) * math_signum(bx) - bx) * alpha
-					bone.scaleY = by + (math_abs(y) * math_signum(by) - by) * alpha
-				elseif blend == MixBlend.add then
-					bx = bone.scaleX
-					by = bone.scaleY
-					bone.scaleX = bx + (math_abs(x) * math_signum(bx) - bone.data.scaleX) * alpha
-					bone.scaleY = by + (math_abs(y) * math_signum(by) - bone.data.scaleY) * alpha
-				end
-			else
-				if blend == MixBlend.setup then
-					bx = math_abs(bone.data.scaleX) * math_signum(x)
-					by = math_abs(bone.data.scaleY) * math_signum(y)
-					bone.scaleX = bx + (x - bx) * alpha
-					bone.scaleY = by + (y - by) * alpha
-				elseif blend == MixBlend.first or blend == MixBlend.replace then
-					bx = math_abs(bone.scaleX) * math_signum(x)
-					by = math_abs(bone.scaleY) * math_signum(y)
-					bone.scaleX = bx + (x - bx) * alpha
-					bone.scaleY = by + (y - by) * alpha
-				elseif blend == MixBlend.add then
-					bx = math_signum(x)
-					by = math_signum(y)
-					bone.scaleX = math_abs(bone.scaleX) * bx + (x - math_abs(bone.data.scaleX) * bx) * alpha
-					bone.scaleY = math_abs(bone.scaleY) * by + (y - math_abs(bone.data.scaleY) * by) * alpha
-				end
-			end
-		end
-	end
-
-	return self
-end
-
-Animation.ShearTimeline = {}
-Animation.ShearTimeline.ENTRIES = Animation.TranslateTimeline.ENTRIES
-function Animation.ShearTimeline.new (frameCount)
-	local ENTRIES = Animation.ShearTimeline.ENTRIES
-	local PREV_TIME = -3
-	local PREV_X = -2
-	local PREV_Y = -1
-	local X = 1
-	local Y = 2
-
-	local self = Animation.TranslateTimeline.new(frameCount)
-	self.type = TimelineType.shear
-
-	function self:getPropertyId ()
-		return TimelineType.shear * SHL_24 + self.boneIndex
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local bone = skeleton.bones[self.boneIndex]
-		if not bone.active then return end
-
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				bone.shearX = bone.data.shearX
-				bone.shearY = bone.data.shearY
-			elseif blend == MixBlend.first then
-				bone.shearX = bone.shearX + (bone.data.shearX - bone.shearX) * alpha
-				bone.shearY = bone.shearX + (bone.data.shearY - bone.shearY) * alpha
-			end
-			return
-		end
-
-		local x = 0
-		local y = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- // Time is after last frame.
-			x = frames[zlen(frames) + PREV_X]
-			y = frames[zlen(frames) + PREV_Y]
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			x = frames[frame + PREV_X]
-			y = frames[frame + PREV_Y]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			x = x + (frames[frame + X] - x) * percent
-			y = y + (frames[frame + Y] - y) * percent
-		end
-		if blend == MixBlend.setup then
-			bone.shearX = bone.data.shearX + x * alpha
-			bone.shearY = bone.data.shearY + y * alpha
-		elseif blend == MixBlend.first or blend == MixBlend.replace then
-			bone.shearX = bone.shearX + (bone.data.shearX + x - bone.shearX) * alpha
-			bone.shearY = bone.shearY + (bone.data.shearY + y - bone.shearY) * alpha
-		elseif blend == MixBlend.add then
-			bone.shearX = bone.shearX + x * alpha
-			bone.shearY = bone.shearY + y * alpha
-		end
-	end
-
-	return self
-end
-
-Animation.ColorTimeline = {}
-Animation.ColorTimeline.ENTRIES = 5
-function Animation.ColorTimeline.new (frameCount)
-	local ENTRIES = Animation.ColorTimeline.ENTRIES
-	local PREV_TIME = -5
-	local PREV_R = -4
-	local PREV_G = -3
-	local PREV_B = -2
-	local PREV_A = -1
-	local R = 1
-	local G = 2
-	local B = 3
-	local A = 4
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.slotIndex = -1
-	self.type = TimelineType.color
-
-	function self:getPropertyId ()
-		return TimelineType.color * SHL_24 + self.slotIndex
-	end
-
-	function self:setFrame (frameIndex, time, r, g, b, a)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + R] = r
-		self.frames[frameIndex + G] = g
-		self.frames[frameIndex + B] = b
-		self.frames[frameIndex + A] = a
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-		local slot = skeleton.slots[self.slotIndex]
-		if not slot.bone.active then return end
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				slot.color:setFrom(slot.data.color)
-			elseif blend == MixBlend.first then
-				local color = slot.color
-				local setup = slot.data.color
-				color:add((setup.r - color.r) * alpha, (setup.g - color.g) * alpha, (setup.b - color.b) * alpha,
-						(setup.a - color.a) * alpha)
-			end
-			return
-		end
-
-		local r, g, b, a
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			local i = zlen(frames)
-			r = frames[i + PREV_R]
-			g = frames[i + PREV_G]
-			b = frames[i + PREV_B]
-			a = frames[i + PREV_A]
-		else
-			-- Interpolate between the last frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			r = frames[frame + PREV_R]
-			g = frames[frame + PREV_G]
-			b = frames[frame + PREV_B]
-			a = frames[frame + PREV_A]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math.floor(frame / ENTRIES) - 1,
-					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			r = r + (frames[frame + R] - r) * percent
-			g = g + (frames[frame + G] - g) * percent
-			b = b + (frames[frame + B] - b) * percent
-			a = a + (frames[frame + A] - a) * percent
-		end
-		if alpha == 1 then
-			slot.color:set(r, g, b, a)
-		else
-			local color = slot.color
-			if blend == MixBlend.setup then color:setFrom(slot.data.color) end
-			color:add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha)
-		end
-	end
-
-	return self
-end
-
-Animation.TwoColorTimeline = {}
-Animation.TwoColorTimeline.ENTRIES = 8
-function Animation.TwoColorTimeline.new (frameCount)
-	local ENTRIES = Animation.TwoColorTimeline.ENTRIES
-	local PREV_TIME = -8
-	local PREV_R = -7
-	local PREV_G = -6
-	local PREV_B = -5
-	local PREV_A = -4
-	local PREV_R2 = -3
-	local PREV_G2 = -2
-	local PREV_B2 = -1
-	local R = 1
-	local G = 2
-	local B = 3
-	local A = 4
-	local R2 = 5
-	local G2 = 6
-	local B2 = 7
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.slotIndex = -1
-	self.type = TimelineType.twoColor
-
-	function self:getPropertyId ()
-		return TimelineType.twoColor * SHL_24 + self.slotIndex
-	end
-
-	function self:setFrame (frameIndex, time, r, g, b, a, r2, g2, b2)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + R] = r
-		self.frames[frameIndex + G] = g
-		self.frames[frameIndex + B] = b
-		self.frames[frameIndex + A] = a
-		self.frames[frameIndex + R2] = r2
-		self.frames[frameIndex + G2] = g2
-		self.frames[frameIndex + B2] = b2
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-		local slot = skeleton.slots[self.slotIndex]
-		if not slot.bone.active then return end
-
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				slot.color:setFrom(slot.data.color)
-				slot.darkColor:setFrom(slot.data.darkColor)
-			elseif blend == MixBlend.first then
-				local light = slot.color
-				local dark = slot.darkColor
-				local setupLight = slot.data.color
-				local setupDark = slot.data.darkColor
-				light:add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha,
-					(setupLight.a - light.a) * alpha)
-				dark:add((setupDark.r - dark.r) * alpha, (setupDark.g - dark.g) * alpha, (setupDark.b - dark.b) * alpha, 0)
-			end
-			return
-		end
-
-		local r, g, b, a, r2, g2, b2
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			local i = zlen(frames)
-			r = frames[i + PREV_R]
-			g = frames[i + PREV_G]
-			b = frames[i + PREV_B]
-			a = frames[i + PREV_A]
-			r2 = frames[i + PREV_R2]
-			g2 = frames[i + PREV_G2]
-			b2 = frames[i + PREV_B2]
-		else
-			-- Interpolate between the last frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			r = frames[frame + PREV_R]
-			g = frames[frame + PREV_G]
-			b = frames[frame + PREV_B]
-			a = frames[frame + PREV_A]
-			r2 = frames[frame + PREV_R2]
-			g2 = frames[frame + PREV_G2]
-			b2 = frames[frame + PREV_B2]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math.floor(frame / ENTRIES) - 1,
-					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			r = r + (frames[frame + R] - r) * percent
-			g = g + (frames[frame + G] - g) * percent
-			b = b + (frames[frame + B] - b) * percent
-			a = a + (frames[frame + A] - a) * percent
-			r2 = r2 + (frames[frame + R2] - r2) * percent
-			g2 = g2 + (frames[frame + G2] - g2) * percent
-			b2 = b2 + (frames[frame + B2] - b2) * percent
-		end
-		if alpha == 1 then
-			slot.color:set(r, g, b, a)
-			slot.darkColor:set(r2, g2, b2, 1)
-		else
-			local light = slot.color
-			local dark = slot.darkColor
-			if blend == MixBlend.setup then
-				light:setFrom(slot.data.color)
-				dark:setFrom(slot.data.darkColor)
-			end
-			light:add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha)
-			dark:add((r2 - dark.r) * alpha, (g2 - dark.g) * alpha, (b2 - dark.b) * alpha, 0)
-		end
-	end
-
-	return self
-end
-
-Animation.AttachmentTimeline = {}
-function Animation.AttachmentTimeline.new (frameCount)
-	local self = {
-		frames = utils.newNumberArrayZero(frameCount), -- time, ...
-		attachmentNames = {},
-		slotIndex = -1,
-		type = TimelineType.attachment
-	}
-
-	function self:getFrameCount ()
-		return zlen(self.frames)
-	end
-
-	function self:setFrame (frameIndex, time, attachmentName)
-		self.frames[frameIndex] = time
-		self.attachmentNames[frameIndex] = attachmentName
-	end
-
-	function self:getPropertyId ()
-		return TimelineType.attachment * SHL_24 + self.slotIndex
-	end
-
-	function self:setAttachment(skeleton, slot, attachmentName)
-		attachmentName = slot.data.attachmentName
-		if not attachmentName then
-			slot:setAttachment(nil)
-		else
-			slot:setAttachment(skeleton:getAttachmentByIndex(self.slotIndex, attachmentName))
-		end
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local slot = skeleton.slots[self.slotIndex]
-		if not slot.bone.active then return end
-		local attachmentName
-		if direction == MixDirection.out then
-			if blend == MixBlend.setup then
-				self:setAttachment(skeleton, slot, slot.data.attachmentName)
-			end
-			return;
-		end
-
-		local frames = self.frames
-		if time < frames[0] then
-			if blend == MixBlend.setup or blend == MixBlend.first then
-				self:setAttachment(skeleton, slot, slot.data.attachmentName)
-			end
-			return
-		end
-
-		local frameIndex = 0
-		if time >= frames[zlen(frames) - 1] then
-			frameIndex = zlen(frames) - 1
-		else
-			frameIndex = binarySearch(frames, time, 1) - 1
-		end
-
-		attachmentName = self.attachmentNames[frameIndex]
-		if not attachmentName then
-			slot:setAttachment(nil)
-		else
-			slot:setAttachment(skeleton:getAttachmentByIndex(self.slotIndex, attachmentName))
-		end
-	end
-
-	return self
-end
-
-Animation.DeformTimeline = {}
-function Animation.DeformTimeline.new (frameCount)
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount)
-	self.frameVertices = utils.newNumberArrayZero(frameCount)
-	self.slotIndex = -1
-	self.attachment = nil
-	self.type = TimelineType.deform
-
-	function self:getPropertyId ()
-		return TimelineType.deform * SHL_27 + self.attachment.id + self.slotIndex
-	end
-
-	function self:setFrame (frameIndex, time, vertices)
-		self.frames[frameIndex] = time
-		self.frameVertices[frameIndex] = vertices
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local slot = skeleton.slots[self.slotIndex]
-		if not slot.bone.active then return end
-
-		local slotAttachment = slot.attachment
-		if not slotAttachment then return end
-		if not (slotAttachment.type == AttachmentType.mesh or slotAttachment.type == AttachmentType.linkedmesh or slotAttachment.type == AttachmentType.path or slotAttachment.type == AttachmentType.boundingbox) then return end
-		if slotAttachment.deformAttachment ~= self.attachment then return end
-
-		local frames = self.frames
-		local deformArray = slot.deform
-		if #(deformArray) == 0 then blend = MixBlend.setup end
-
-		local frameVertices = self.frameVertices
-		local vertexCount = #(frameVertices[0])
-
-		if time < frames[0] then
-			local vertexAttachment = slotAttachment;
-			if blend == MixBlend.setup then
-				slot.deform = {}
-				return;
-			elseif blend == MixBlend.first then
-				if (alpha == 1) then
-					slot.deform = {}
-					return;
-				end
-
-				local deform = utils.setArraySize(deformArray, vertexCount)
-				if (vertexAttachment.bones == nil) then
-					local setupVertices = vertexAttachment.vertices
-					local i = 1
-					while i <= vertexCount do
-						deform[i] = deform[i] + (setupVertices[i] - deform[i]) * alpha
-						i = i + 1
-					end
-				else
-					alpha = 1 - alpha
-					local i = 1
-					while i <= vertexCount do
-						deform[i] = deform[i] * alpha
-						i = i + 1
-					end
-				end
-			end
-			return
-		end
-
-		local deform = utils.setArraySize(deformArray, vertexCount)
-		if time >= frames[zlen(frames) - 1] then -- Time is after last frame.
-			local lastVertices = frameVertices[zlen(frames) - 1]
-			if alpha == 1 then
-				if blend == MixBlend.add then
-					local vertexAttachment = slotAttachment
-					if vertexAttachment.bones == nil then
-						-- Unweighted vertex positions, with alpha.
-						local setupVertices = vertexAttachment.vertices
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = deform[i] + lastVertices[i] - setupVertices[i]
-							i = i + 1
-						end
-					else
-						-- Weighted deform offsets, with alpha.
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = deform[i] + lastVertices[i]
-							i = i + 1
-						end
-					end
-				else
-					local i = 1
-					while i <= vertexCount do
-						deform[i] = lastVertices[i]
-						i = i + 1
-					end
-				end
-			else
-				if blend == MixBlend.setup then
-					local vertexAttachment = slotAttachment
-					if vertexAttachment.bones == nil then
-						-- Unweighted vertex positions, with alpha.
-						local setupVertices = vertexAttachment.vertices
-						local i = 1
-						while i <= vertexCount do
-							local setup = setupVertices[i]
-							deform[i] = setup + (lastVertices[i] - setup) * alpha
-							i = i + 1
-						end
-					else
-						-- Weighted deform offsets, with alpha.
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = lastVertices[i] * alpha
-							i = i + 1
-						end
-					end
-				elseif blend == MixBlend.first or blend == MixBlend.replace then
-					local i = 1
-					while i <= vertexCount do
-						deform[i] = deform[i] + (lastVertices[i] - deform[i]) * alpha
-						i = i + 1
-					end
-					local vertexAttachment = slotAttachment
-					if vertexAttachment.bones == nil then
-						local setupVertices = vertexAttachment.vertices
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = deform[i] + (lastVertices[i] - setupVertices[i]) * alpha
-							i = i + 1
-						end
-					else
-						-- Weighted deform offsets, with alpha.
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = deform[i] + lastVertices[i] * alpha
-							i = i + 1
-						end
-					end
-				elseif blend == MixBlend.add then
-					local vertexAttachment = slotAttachment
-					if vertexAttachment.bones == nil then
-						local setupVertices = vertexAttachment.vertices
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = deform[i] + (lastVertices[i] - setupVertices[i]) * alpha
-							i = i + 1
-						end
-					else
-						-- Weighted deform offsets, with alpha.
-						local i = 1
-						while i <= vertexCount do
-							deform[i] = deform[i] + lastVertices[i] * alpha
-							i = i + 1
-						end
-					end
-				end
-			end
-			return;
-		end
-
-		-- Interpolate between the previous frame and the current frame.
-		local frame = binarySearch(frames, time, 1)
-		local prevVertices = frameVertices[frame - 1]
-		local nextVertices = frameVertices[frame]
-		local frameTime = frames[frame]
-		local percent = self:getCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime))
-
-		if alpha == 1 then
-			if blend == MixBlend.add then
-				local vertexAttachment = slotAttachment
-				if vertexAttachment.bones == nil then
-					-- Unweighted vertex positions, with alpha.
-					local setupVertices = vertexAttachment.vertices
-					local i = 1
-					while i <= vertexCount do
-						local prev = prevVertices[i]
-						deform[i] = deform[i] + prev + (nextVertices[i] - prev) * precent - setupVertices[i]
-						i = i + 1
-					end
-				else
-					-- Weighted deform offsets, with alpha.
-					local i = 1
-					while i <= vertexCount do
-						local prev = prevVertices[i]
-						deform[i] = deform[i] + prev + (nextVertices[i] - prev) * percent
-						i = i + 1
-					end
-				end
-			else
-				local i = 1
-				while i <= vertexCount do
-					local prev = prevVertices[i]
-					deform[i] = prev + (nextVertices[i] - prev) * percent
-					i = i + 1
-				end
-			end
-		else
-			if blend == MixBlend.setup then
-				local vertexAttachment = slotAttachment
-				if vertexAttachment.bones == nil then
-					-- Unweighted vertex positions, with alpha.
-					local setupVertices = vertexAttachment.vertices
-					local i = 1
-					while i <= vertexCount do
-						local prev = prevVertices[i]
-						local setup = setupVertices[i]
-						deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha
-						i = i + 1
-					end
-				else
-					-- Weighted deform offsets, with alpha.
-					local i = 1
-					while i <= vertexCount do
-						local prev = prevVertices[i]
-						deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha
-						i = i + 1
-					end
-				end
-			elseif blend == MixBlend.first or blend == MixBlend.replace then
-				local i = 1
-				while i <= vertexCount do
-					local prev = prevVertices[i]
-					deform[i] = deform[i] + (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha
-					i = i + 1
-				end
-			elseif blend == MixBlend.add then
-				local vertexAttachment = slotAttachment
-				if vertexAttachment.bones == nil then
-					local setupVertices = vertexAttachment.vertices
-					local i = 1
-					while i <= vertexCount do
-						local prev = prevVertices[i]
-						deform[i] = deform[i] + (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha
-						i = i + 1
-					end
-				else
-					-- Weighted deform offsets, with alpha.
-					local i = 1
-					while i <= vertexCount do
-						local prev = prevVertices[i]
-						deform[i] = deform[i] + (prev + (nextVertices[i] - prev) * percent) * alpha
-						i = i + 1
-					end
-				end
-			end
-		end
-	end
-
-	return self
-end
-
-Animation.EventTimeline = {}
-function Animation.EventTimeline.new (frameCount)
-	local self = {
-		frames = utils.newNumberArrayZero(frameCount),
-		events = {},
-		type = TimelineType.event
-	}
-
-	function self:getPropertyId ()
-		return TimelineType.event * SHL_24
-	end
-
-	function self:getFrameCount ()
-		return zlen(self.frames)
-	end
-
-	function self:setFrame (frameIndex, event)
-		self.frames[frameIndex] = event.time
-		self.events[frameIndex] = event
-	end
-
-	-- Fires events for frames > lastTime and <= time.
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		if not firedEvents then return end
-
-		local frames = self.frames
-		local frameCount = zlen(frames)
-
-		if lastTime > time then -- Fire events after last time for looped animations.
-			self:apply(skeleton, lastTime, 999999, firedEvents, alpha, blend, direction)
-			lastTime = -1
-		elseif lastTime >= frames[frameCount - 1] then -- Last time is after last frame.
-			return
-		end
-		if time < frames[0] then return end -- Time is before first frame.
-
-		local frame
-		if lastTime < frames[0] then
-			frame = 0
-		else
-			frame = binarySearch1(frames, lastTime)
-			local frame = frames[frame]
-			while frame > 0 do -- Fire multiple events with the same frame.
-				if frames[frame - 1] ~= frame then break end
-				frame = frame - 1
-			end
-		end
-		local events = self.events
-		while frame < frameCount and time >= frames[frame] do
-			table.insert(firedEvents, events[frame])
-			frame = frame + 1
-		end
-	end
-
-	return self
-end
-
-Animation.DrawOrderTimeline = {}
-function Animation.DrawOrderTimeline.new (frameCount)
-	local self = {
-		frames = utils.newNumberArrayZero(frameCount),
-		drawOrders = {},
-		type = TimelineType.drawOrder
-	}
-
-	function self:getPropertyId ()
-		return TimelineType.drawOrder * SHL_24
-	end
-
-	function self:getFrameCount ()
-		return zlen(self.frames)
-	end
-
-	function self:setFrame (frameIndex, time, drawOrder)
-		self.frames[frameIndex] = time
-		self.drawOrders[frameIndex] = drawOrder
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local drawOrder = skeleton.drawOrder
-		local slots = skeleton.slots
-		if direction == MixDirection.out then
-			if blend == MixBlend.setup then
-				for i,slot in ipairs(slots) do
-					drawOrder[i] = slots[i]
-				end
-			end
-			return;
-		end
-
-		local frames = self.frames
-		if time < frames[0] then
-			if blend == MixBlend.setup or blend == MixBlend.first then
-				for i,slot in ipairs(slots) do
-					drawOrder[i] = slots[i]
-				end
-			end
-			return
-		end
-
-		local frame
-		if time >= frames[zlen(frames) - 1] then -- Time is after last frame.
-			frame = zlen(frames) - 1
-		else
-			frame = binarySearch1(frames, time) - 1
-		end
-
-		local drawOrderToSetupIndex = self.drawOrders[frame]
-		if not drawOrderToSetupIndex then
-			for i,slot in ipairs(slots) do
-				drawOrder[i] = slots[i]
-			end
-		else
-			for i,setupIndex in ipairs(drawOrderToSetupIndex) do
-				drawOrder[i] = skeleton.slots[setupIndex]
-			end
-		end
-	end
-
-	return self
-end
-
-Animation.IkConstraintTimeline = {}
-Animation.IkConstraintTimeline.ENTRIES = 6
-function Animation.IkConstraintTimeline.new (frameCount)
-	local ENTRIES = Animation.IkConstraintTimeline.ENTRIES
-	local PREV_TIME = -6
-	local PREV_MIX = -5
-	local PREV_SOFTNESS = -4
-	local PREV_BEND_DIRECTION = -3
-	local PREV_COMPRESS = -2
-	local PREV_STRETCH = -1
-	local MIX = 1
-	local SOFTNESS = 2
-	local BEND_DIRECTION = 3
-	local COMPRESS = 4
-	local STRETCH = 5
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES) -- time, mix, softness, bendDirection, compress, stretch, ...
-	self.ikConstraintIndex = -1
-	self.type = TimelineType.ikConstraint
-
-	function self:getPropertyId ()
-		return TimelineType.ikConstraint * SHL_24 + self.ikConstraintIndex
-	end
-
-	function self:setFrame (frameIndex, time, mix, softness, bendDirection, compress, stretch)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + MIX] = mix
-		self.frames[frameIndex + SOFTNESS] = softness
-		self.frames[frameIndex + BEND_DIRECTION] = bendDirection
-		if (compress) then
-			self.frames[frameIndex + COMPRESS] = 1
-		else
-			self.frames[frameIndex + COMPRESS] = 0
-		end
-		if (stretch) then
-			self.frames[frameIndex + STRETCH] = 1
-		else
-			self.frames[frameIndex + STRETCH] = 0
-		end
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local constraint = skeleton.ikConstraints[self.ikConstraintIndex]
-		if not constraint.active then return end
-		if time < frames[0] then
-			if blend == MixBlend.setup then
-				constraint.mix = constraint.data.mix
-				constraint.softness = constraint.data.softness
-				constraint.bendDirection = constraint.data.bendDirection
-				constraint.compress = constraint.data.compress
-				constraint.stretch = constraint.data.stretch
-			elseif blend == MixBlend.first then
-				constraint.mix = constraint.mix + (constraint.data.mix - constraint.mix) * alpha
-				constraint.softness = constraint.softness + (constraint.data.softness - constraint.softness) * alpha
-				constraint.bendDirection = constraint.data.bendDirection
-				constraint.compress = constraint.data.compress
-				constraint.stretch = constraint.data.stretch
-			end
-			return
-		end
-
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			if blend == MixBlend.setup then
-				constraint.mix = constraint.data.mix + (frames[zlen(frames) + PREV_MIX] - constraint.data.mix) * alpha
-				constraint.softness = constraint.data.softness + (frames[zlen(frames) + PREV_SOFTNESS] - constraint.data.softness) * alpha
-				if direction == MixDirection.out then
-					constraint.bendDirection = constraint.data.bendDirection
-					constraint.compress = constraint.data.compress
-					constraint.stretch = constraint.data.stretch
-				else
-					constraint.bendDirection = math_floor(frames[zlen(frames) + PREV_BEND_DIRECTION]);
-					if (math_floor(frames[zlen(frames) + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
-					if (math_floor(frames[zlen(frames) + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
-				end
-			else
-				constraint.mix = constraint.mix + (frames[zlen(frames) + PREV_MIX] - constraint.mix) * alpha
-				constraint.softness = constraint.softness + (frames[zlen(frames) + PREV_SOFTNESS] - constraint.softness) * alpha
-				if direction == MixDirection._in then
-					constraint.bendDirection = math_floor(frames[zlen(frames) + PREV_BEND_DIRECTION])
-					if (math_floor(frames[zlen(frames) + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
-					if (math_floor(frames[zlen(frames) + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
-				end
-			end
-			return
-		end
-
-		-- Interpolate between the previous frame and the current frame.
-		local frame = binarySearch(frames, time, ENTRIES)
-		local mix = frames[frame + PREV_MIX]
-		local softness = frames[frame + PREV_SOFTNESS]
-		local frameTime = frames[frame]
-		local percent = self:getCurvePercent(math.floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-		if blend == MixBlend.setup then
-			constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha
-			constraint.softness = constraint.data.softness + (softness + (frames[frame + SOFTNESS] - softness) * percent - constraint.data.softness) * alpha
-			if direction == MixDirection.out then
-				constraint.bendDirection = constraint.data.bendDirection
-				constraint.compress = constraint.data.compress
-				constraint.stretch = constraint.data.stretch
-			else
-				constraint.bendDirection = math_floor(frames[frame + PREV_BEND_DIRECTION])
-				if (math_floor(frames[frame + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
-				if (math_floor(frames[frame + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
-			end
-		else
-			constraint.mix = constraint.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha
-			constraint.softness = constraint.softness + (softness + (frames[frame + SOFTNESS] - softness) * percent - constraint.softness) * alpha
-			if direction == MixDirection._in then
-				constraint.bendDirection = math_floor(frames[frame + PREV_BEND_DIRECTION])
-				if (math_floor(frames[frame + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
-				if (math_floor(frames[frame + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
-			end
-		end
-	end
-
-	return self
-end
-
-Animation.TransformConstraintTimeline = {}
-Animation.TransformConstraintTimeline.ENTRIES = 5
-function Animation.TransformConstraintTimeline.new (frameCount)
-	local ENTRIES = Animation.TransformConstraintTimeline.ENTRIES
-	local PREV_TIME = -5
-	local PREV_ROTATE = -4
-	local PREV_TRANSLATE = -3
-	local PREV_SCALE = -2
-	local PREV_SHEAR = -1
-	local ROTATE = 1
-	local TRANSLATE = 2
-	local SCALE = 3
-	local SHEAR = 4
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.transformConstraintIndex = -1
-	self.type = TimelineType.transformConstraint
-
-	function self:getPropertyId ()
-		return TimelineType.transformConstraint * SHL_24 + self.transformConstraintIndex
-	end
-
-	function self:setFrame (frameIndex, time, rotateMix, translateMix, scaleMix, shearMix)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + ROTATE] = rotateMix
-		self.frames[frameIndex + TRANSLATE] = translateMix
-		self.frames[frameIndex + SCALE] = scaleMix
-		self.frames[frameIndex + SHEAR] = shearMix
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local constraint = skeleton.transformConstraints[self.transformConstraintIndex]
-		if not constraint.active then return end
-
-		if time < frames[0] then
-			local data = constraint.data
-			if blend == MixBlend.setup then
-				constraint.rotateMix = data.rotateMix
-				constraint.translateMix = data.translateMix
-				constraint.scaleMix = data.scaleMix
-				constraint.shearMix = data.shearMix
-			elseif blend == MixBlend.first then
-				constraint.rotateMix = constraint.rotateMix + (data.rotateMix - constraint.rotateMix) * alpha
-				constraint.translateMix = constraint.translateMix + (data.translateMix - constraint.translateMix) * alpha
-				constraint.scaleMix = constraint.scaleMix + (data.scaleMix - constraint.scaleMix) * alpha
-				constraint.shearMix = constraint.shearMix + (data.shearMix - constraint.shearMix) * alpha
-			end
-			return
-		end
-
-		local rotate = 0
-		local translate = 0
-		local scale = 0
-		local shear = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			local i = zlen(frames)
-			rotate = frames[i + PREV_ROTATE]
-			translate = frames[i + PREV_TRANSLATE]
-			scale = frames[i + PREV_SCALE]
-			shear = frames[i + PREV_SHEAR]
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			rotate = frames[frame + PREV_ROTATE]
-			translate = frames[frame + PREV_TRANSLATE]
-			scale = frames[frame + PREV_SCALE]
-			shear = frames[frame + PREV_SHEAR]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime));
-
-			rotate = rotate + (frames[frame + ROTATE] - rotate) * percent
-			translate = translate + (frames[frame + TRANSLATE] - translate) * percent
-			scale = scale + (frames[frame + SCALE] - scale) * percent
-			shear = shear + (frames[frame + SHEAR] - shear) * percent
-		end
-		if blend == MixBlend.setup then
-			local data = constraint.data
-			constraint.rotateMix = data.rotateMix + (rotate - data.rotateMix) * alpha
-			constraint.translateMix = data.translateMix + (translate - data.translateMix) * alpha
-			constraint.scaleMix = data.scaleMix + (scale - data.scaleMix) * alpha
-			constraint.shearMix = data.shearMix + (shear - data.shearMix) * alpha
-		else
-			constraint.rotateMix = constraint.rotateMix + (rotate - constraint.rotateMix) * alpha
-			constraint.translateMix = constraint.translateMix + (translate - constraint.translateMix) * alpha
-			constraint.scaleMix = constraint.scaleMix + (scale - constraint.scaleMix) * alpha
-			constraint.shearMix = constraint.shearMix + (shear - constraint.shearMix) * alpha
-		end
-	end
-
-	return self
-end
-
-Animation.PathConstraintPositionTimeline = {}
-Animation.PathConstraintPositionTimeline.ENTRIES = 2
-function Animation.PathConstraintPositionTimeline.new (frameCount)
-	local ENTRIES = Animation.PathConstraintPositionTimeline.ENTRIES
-	local PREV_TIME = -2
-	local PREV_VALUE = -1
-	local VALUE = 1
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.pathConstraintIndex = -1
-	self.type = TimelineType.pathConstraintPosition
-
-	function self:getPropertyId ()
-		return TimelineType.pathConstraintPosition * SHL_24 + self.pathConstraintIndex
-	end
-
-	function self:setFrame (frameIndex, time, value)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + VALUE] = value
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local constraint = skeleton.pathConstraints[self.pathConstraintIndex]
-		if not constraint.active then return end
-
-		if (time < frames[0]) then
-			if blend == MixBlend.setup then
-				constraint.position = constraint.data.position
-			elseif blend == MixBlend.first then
-				constraint.position = constraint.position + (constraint.data.position - constraint.position) * alpha
-			end
-			return
-		end
-
-		local position = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			position = frames[zlen(frames) + PREV_VALUE]
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			position = frames[frame + PREV_VALUE]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			position = position + (frames[frame + VALUE] - position) * percent
-		end
-		if blend == MixBlend.setup then
-			constraint.position = constraint.data.position + (position - constraint.data.position) * alpha
-		else
-			constraint.position = constraint.position + (position - constraint.position) * alpha
-		end
-	end
-
-	return self
-end
-
-Animation.PathConstraintSpacingTimeline = {}
-Animation.PathConstraintSpacingTimeline.ENTRIES = 2
-function Animation.PathConstraintSpacingTimeline.new (frameCount)
-	local ENTRIES = Animation.PathConstraintSpacingTimeline.ENTRIES
-	local PREV_TIME = -2
-	local PREV_VALUE = -1
-	local VALUE = 1
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.pathConstraintIndex = -1
-	self.type = TimelineType.pathConstraintSpacing
-
-	function self:getPropertyId ()
-		return TimelineType.pathConstraintSpacing * SHL_24 + self.pathConstraintIndex
-	end
-
-	function self:setFrame (frameIndex, time, value)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + VALUE] = value
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local constraint = skeleton.pathConstraints[self.pathConstraintIndex]
-		if not constraint.active then return end
-
-		if (time < frames[0]) then
-			if blend == MixBlend.setup then
-				constraint.spacing = constraint.data.spacing
-			elseif blend == MixBlend.first then
-				constraint.spacing = constraint.spacing + (constraint.data.spacing - constraint.spacing) * alpha
-			end
-			return
-		end
-
-		local spacing = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			spacing = frames[zlen(frames) + PREV_VALUE]
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			spacing = frames[frame + PREV_VALUE]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			spacing = spacing + (frames[frame + VALUE] - spacing) * percent
-		end
-
-		if blend == MixBlend.setup then
-			constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha
-		else
-			constraint.spacing = constraint.spacing + (spacing - constraint.spacing) * alpha
-		end
-	end
-
-	return self
-end
-
-Animation.PathConstraintMixTimeline = {}
-Animation.PathConstraintMixTimeline.ENTRIES = 3
-function Animation.PathConstraintMixTimeline.new (frameCount)
-	local ENTRIES = Animation.PathConstraintMixTimeline.ENTRIES
-	local PREV_TIME = -3
-	local PREV_ROTATE = -2
-	local PREV_TRANSLATE = -1
-	local ROTATE = 1
-	local TRANSLATE = 2
-
-	local self = Animation.CurveTimeline.new(frameCount)
-	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
-	self.pathConstraintIndex = -1
-	self.type = TimelineType.pathConstraintMix
-
-	function self:getPropertyId ()
-		return TimelineType.pathConstraintMix * SHL_24 + self.pathConstraintIndex
-	end
-
-	function self:setFrame (frameIndex, time, rotateMix, translateMix)
-		frameIndex = frameIndex * ENTRIES
-		self.frames[frameIndex] = time
-		self.frames[frameIndex + ROTATE] = rotateMix
-		self.frames[frameIndex + TRANSLATE] = translateMix
-	end
-
-	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
-		local frames = self.frames
-
-		local constraint = skeleton.pathConstraints[self.pathConstraintIndex]
-		if not constraint.active then return end
-
-		if (time < frames[0]) then
-			if blend == MixBlend.setup then
-				constraint.rotateMix = constraint.data.rotateMix
-				constraint.translateMix = constraint.data.translateMix
-			elseif blend == MixBlend.first then
-				constraint.rotateMix = constraint.rotateMix + (constraint.data.rotateMix - constraint.rotateMix) * alpha
-				constraint.translateMix = constraint.translateMix + (constraint.data.translateMix - constraint.translateMix) * alpha
-			end
-			return
-		end
-
-		local rotate = 0
-		local translate = 0
-		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
-			rotate = frames[zlen(frames) + PREV_ROTATE]
-			translate = frames[zlen(frames) + PREV_TRANSLATE]
-		else
-			-- Interpolate between the previous frame and the current frame.
-			local frame = binarySearch(frames, time, ENTRIES)
-			rotate = frames[frame + PREV_ROTATE]
-			translate = frames[frame + PREV_TRANSLATE]
-			local frameTime = frames[frame]
-			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
-				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
-
-			rotate = rotate + (frames[frame + ROTATE] - rotate) * percent
-			translate = translate + (frames[frame + TRANSLATE] - translate) * percent
-		end
-
-		if blend == MixBlend.setup then
-			constraint.rotateMix = constraint.data.rotateMix + (rotate - constraint.data.rotateMix) * alpha
-			constraint.translateMix = constraint.data.translateMix + (translate - constraint.data.translateMix) * alpha
-		else
-			constraint.rotateMix = constraint.rotateMix + (rotate - constraint.rotateMix) * alpha
-			constraint.translateMix = constraint.translateMix + (translate - constraint.translateMix) * alpha
-		end
-	end
-
-	return self
-end
-
-return Animation
+-------------------------------------------------------------------------------
+-- Spine Runtimes License Agreement
+-- Last updated January 1, 2020. Replaces all prior versions.
+--
+-- Copyright (c) 2013-2020, Esoteric Software LLC
+--
+-- Integration of the Spine Runtimes into software or otherwise creating
+-- derivative works of the Spine Runtimes is permitted under the terms and
+-- conditions of Section 2 of the Spine Editor License Agreement:
+-- http://esotericsoftware.com/spine-editor-license
+--
+-- Otherwise, it is permitted to integrate the Spine Runtimes into software
+-- or otherwise create derivative works of the Spine Runtimes (collectively,
+-- "Products"), provided that each user of the Products must obtain their own
+-- Spine Editor license and redistribution of the Products in any form must
+-- include this license and copyright notice.
+--
+-- THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
+-- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+-- DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
+-- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+-- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
+-- BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
+-- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+-- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
+-- THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-------------------------------------------------------------------------------
+
+-- FIXME
+-- All the indexing in this file is zero based. We use zlen()
+-- instead of the # operator. Initialization of number arrays
+-- is performed via utils.newNumberArrayZero. This needs
+-- to be rewritten using one-based indexing for better performance
+
+local utils = require "spine-lua.utils"
+local AttachmentType = require "spine-lua.attachments.AttachmentType"
+local math_floor = math.floor
+local math_abs = math.abs
+local math_signum = utils.signum
+
+local function zlen(array)
+	return #array + 1
+end
+
+local Animation = {}
+function Animation.new (name, timelines, duration)
+	if not timelines then error("timelines cannot be nil", 2) end
+
+	local self = {
+		name = name,
+		timelines = timelines,
+		timelineIds = {},
+		duration = duration
+	}
+	
+	for i,timeline in ipairs(self.timelines) do
+		self.timelineIds[timeline:getPropertyId()] = true
+	end
+
+	function self:hasTimeline(id)
+		return self.timelineIds[id] == true
+	end
+
+	function self:apply (skeleton, lastTime, time, loop, events, alpha, blend, direction)
+		if not skeleton then error("skeleton cannot be nil.", 2) end
+
+		if loop and duration > 0 then
+			time = time % self.duration
+			if lastTime > 0 then lastTime = lastTime % self.duration end
+		end
+
+		for i,timeline in ipairs(self.timelines) do
+			timeline:apply(skeleton, lastTime, time, events, alpha, blend, direction)
+		end
+	end
+
+	return self
+end
+
+local function binarySearch (values, target, step)
+	local low = 0
+	local high = math.floor(zlen(values) / step - 2)
+	if high == 0 then return step end
+	local current = math.floor(high / 2)
+	while true do
+		if values[(current + 1) * step] <= target then
+			low = current + 1
+		else
+			high = current
+		end
+		if low == high then return (low + 1) * step end
+		current = math.floor((low + high) / 2)
+	end
+end
+Animation.binarySearch = binarySearch
+
+local function binarySearch1 (values, target)
+	local low = 0
+	local high = math.floor(zlen(values)	- 2)
+	if high == 0 then return 1 end
+	local current = math.floor(high / 2)
+	while true do
+		if values[current + 1] <= target then
+			low = current + 1
+		else
+			high = current
+		end
+		if low == high then return low + 1 end
+		current = math.floor((low + high) / 2)
+	end
+end
+
+local function linearSearch (values, target, step)
+	local i = 0
+	local last = zlen(values) - step
+	while i <= last do
+		if (values[i] > target) then return i end
+		i = i + step
+	end
+	return -1
+end
+
+Animation.MixBlend = {
+	setup = 0,
+	first = 1,
+	replace = 2,
+	add = 3
+}
+local MixBlend = Animation.MixBlend
+
+Animation.MixDirection = {
+	_in = 0, out = 1
+}
+local MixDirection = Animation.MixDirection
+
+Animation.TimelineType = {
+	rotate = 0, translate = 1, scale = 2, shear = 3,
+	attachment = 4, color = 5, deform = 6,
+	event = 7, drawOrder = 8,
+	ikConstraint = 9, transformConstraint = 10,
+	pathConstraintPosition = 11, pathConstraintSpacing = 12, pathConstraintMix = 13,
+	twoColor = 14
+}
+local TimelineType = Animation.TimelineType
+local SHL_24 = 16777216
+local SHL_27 = 134217728
+
+Animation.CurveTimeline = {}
+function Animation.CurveTimeline.new (frameCount)
+	local LINEAR = 0
+	local STEPPED = 1
+	local BEZIER = 2
+	local BEZIER_SIZE = 10 * 2 - 1
+
+	local self = {
+		curves = utils.newNumberArrayZero((frameCount - 1) * BEZIER_SIZE) -- type, x, y, ...
+	}
+
+	function self:getFrameCount ()
+		return math.floor(zlen(self.curves) / BEZIER_SIZE) + 1
+	end
+
+	function self:setStepped (frameIndex)
+		self.curves[frameIndex * BEZIER_SIZE] = STEPPED
+	end
+
+	function self:getCurveType (frameIndex)
+		local index = frameIndex * BEZIER_SIZE
+		if index == zlen(self.curves) then return LINEAR end
+		local type = self.curves[index]
+		if type == LINEAR then return LINEAR end
+		if type == STEPPED then return STEPPED end
+		return BEZIER
+	end
+
+	function self:setCurve (frameIndex, cx1, cy1, cx2, cy2)
+			local tmpx = (-cx1 * 2 + cx2) * 0.03
+			local tmpy = (-cy1 * 2 + cy2) * 0.03
+			local dddfx = ((cx1 - cx2) * 3 + 1) * 0.006
+			local dddfy = ((cy1 - cy2) * 3 + 1) * 0.006
+			local ddfx = tmpx * 2 + dddfx
+			local ddfy = tmpy * 2 + dddfy
+			local dfx = cx1 * 0.3 + tmpx + dddfx * 0.16666667
+			local dfy = cy1 * 0.3 + tmpy + dddfy * 0.16666667
+
+			local i = frameIndex * BEZIER_SIZE
+			local curves = self.curves
+			curves[i] = BEZIER
+			i = i + 1
+
+			local x = dfx
+			local y = dfy
+			local n = i + BEZIER_SIZE - 1
+			while i < n do
+				curves[i] = x
+				curves[i + 1] = y
+				dfx = dfx + ddfx
+				dfy = dfy + ddfy
+				ddfx = ddfx + dddfx
+				ddfy = ddfy + dddfy
+				x = x + dfx
+				y = y + dfy
+				i = i + 2
+			end
+	end
+
+	function self:getCurvePercent (frameIndex, percent)
+		percent = utils.clamp(percent, 0, 1)
+		local curves = self.curves
+		local i = frameIndex * BEZIER_SIZE
+		local type = curves[i]
+		if type == LINEAR then return percent end
+		if type == STEPPED then return 0 end
+		i = i + 1
+		local x
+		local n = i + BEZIER_SIZE - 1
+		local start = i
+		while i < n do
+			x = curves[i]
+			if x >= percent then
+				local prevX, prevY
+				if i == start then
+					prevX = 0
+					prevY = 0
+				else
+					prevX = curves[i - 2]
+					prevY = curves[i - 1]
+				end
+				return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX)
+			end
+			i = i + 2
+		end
+		local y = curves[i - 1]
+		return y + (1 - y) * (percent - x) / (1 - x) -- Last point is 1,1.
+	end
+
+	return self
+end
+
+Animation.RotateTimeline = {}
+Animation.RotateTimeline.ENTRIES = 2
+Animation.RotateTimeline.PREV_TIME = -2
+Animation.RotateTimeline.PREV_ROTATION = -1
+Animation.RotateTimeline.ROTATION = 1
+function Animation.RotateTimeline.new (frameCount)
+	local ENTRIES = Animation.RotateTimeline.ENTRIES
+	local PREV_TIME = Animation.RotateTimeline.PREV_TIME
+	local PREV_ROTATION = Animation.RotateTimeline.PREV_ROTATION
+	local ROTATION = Animation.RotateTimeline.ROTATION
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.boneIndex = -1
+	self.frames = utils.newNumberArrayZero(frameCount * 2)
+	self.type = TimelineType.rotate
+
+	function self:getPropertyId ()
+		return TimelineType.rotate * SHL_24 + self.boneIndex
+	end
+
+	function self:setFrame (frameIndex, time, degrees)
+		frameIndex = frameIndex * 2
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + ROTATION] = degrees
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local bone = skeleton.bones[self.boneIndex]
+		if not bone.active then return end
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				bone.rotation = bone.data.rotation
+			elseif blend == MixBlend.first then
+				local r = bone.data.rotation - bone.rotation
+				bone.rotation = bone.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
+			end
+			return
+		end
+
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			local r = frames[zlen(frames) + PREV_ROTATION]
+			if blend == MixBlend.setup then
+				bone.rotation = bone.data.rotation + r * alpha
+			elseif blend == MixBlend.first or blend == MixBlend.replace then
+				r = r + bone.data.rotation - bone.rotation
+				r = r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360 -- Wrap within -180 and 180.
+				bone.rotation = bone.rotation + r * alpha
+			elseif blend == MixBlend.add then
+				bone.rotation = bone.rotation + r * alpha
+			end
+			return
		end
+
+		-- Interpolate between the last frame and the current frame.
+		local frame = binarySearch(frames, time, ENTRIES)
+		local prevRotation = frames[frame + PREV_ROTATION]
+		local frameTime = frames[frame]
+		local percent = self:getCurvePercent((math.floor(frame / 2)) - 1, 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+		local r = frames[frame + ROTATION] - prevRotation
+		r = prevRotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * percent
+		if blend == MixBlend.setup then
+			bone.rotation = bone.data.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
+		elseif blend == MixBlend.first or blend == MixBlend.replace then
+			r = r + bone.data.rotation - bone.rotation
+			bone.rotation = bone.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
+		elseif blend == MixBlend.add then
+			bone.rotation = bone.rotation + (r - (16384 - math_floor(16384.499999999996 - r / 360)) * 360) * alpha
+		end
+	end
+
+	return self
+end
+
+Animation.TranslateTimeline = {}
+Animation.TranslateTimeline.ENTRIES = 3
+function Animation.TranslateTimeline.new (frameCount)
+	local ENTRIES = Animation.TranslateTimeline.ENTRIES
+	local PREV_TIME = -3
+	local PREV_X = -2
+	local PREV_Y = -1
+	local X = 1
+	local Y = 2
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.boneIndex = -1
+	self.type = TimelineType.translate
+
+	function self:getPropertyId ()
+		return TimelineType.translate * SHL_24 + self.boneIndex
+	end
+
+	function self:setFrame (frameIndex, time, x, y)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + X] = x
+		self.frames[frameIndex + Y] = y
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local bone = skeleton.bones[self.boneIndex]
+		if not bone.active then return end
+
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				bone.x = bone.data.x
+				bone.y = bone.data.y
+			elseif blend == MixBlend.first then
+				bone.x = bone.x + (bone.data.x - bone.x) * alpha
+				bone.y = bone.y + (bone.data.y - bone.y) * alpha
+			end
+			return
+		end
+
+		local x = 0
+		local y = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- // Time is after last frame.
+			x = frames[zlen(frames) + PREV_X]
+			y = frames[zlen(frames) + PREV_Y]
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			x = frames[frame + PREV_X]
+			y = frames[frame + PREV_Y]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			x = x + (frames[frame + X] - x) * percent
+			y = y + (frames[frame + Y] - y) * percent
+		end
+		if blend == MixBlend.setup then
+			bone.x = bone.data.x + x * alpha
+			bone.y = bone.data.y + y * alpha
+		elseif blend == MixBlend.first or blend == MixBlend.replace then
+			bone.x = bone.x + (bone.data.x + x - bone.x) * alpha
+			bone.y = bone.y + (bone.data.y + y - bone.y) * alpha
+		elseif blend == MixBlend.add then
+			bone.x = bone.x + x * alpha
+			bone.y = bone.y + y * alpha
+		end
+	end
+
+	return self
+end
+
+Animation.ScaleTimeline = {}
+Animation.ScaleTimeline.ENTRIES = Animation.TranslateTimeline.ENTRIES
+function Animation.ScaleTimeline.new (frameCount)
+	local ENTRIES = Animation.ScaleTimeline.ENTRIES
+	local PREV_TIME = -3
+	local PREV_X = -2
+	local PREV_Y = -1
+	local X = 1
+	local Y = 2
+
+	local self = Animation.TranslateTimeline.new(frameCount)
+	self.type = TimelineType.scale
+
+	function self:getPropertyId ()
+		return TimelineType.scale * SHL_24 + self.boneIndex
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local bone = skeleton.bones[self.boneIndex]
+		if not bone.active then return end
+
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				bone.scaleX = bone.data.scaleX
+				bone.scaleY = bone.data.scaleY
+			elseif blend == MixBlend.first then
+				bone.scaleX = bone.scaleX + (bone.data.scaleX - bone.scaleX) * alpha
+				bone.scaleY = bone.scaleY + (bone.data.scaleY - bone.scaleY) * alpha
+			end
+			return
+		end
+
+		local x = 0
+		local y = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			x = frames[zlen(frames) + PREV_X] * bone.data.scaleX
+			y = frames[zlen(frames) + PREV_Y] * bone.data.scaleY
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			x = frames[frame + PREV_X]
+			y = frames[frame + PREV_Y]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			x = (x + (frames[frame + X] - x) * percent) * bone.data.scaleX
+			y = (y + (frames[frame + Y] - y) * percent) * bone.data.scaleY
+		end
+		if alpha == 1 then
+			if blend == MixBlend.add then
+				bone.scaleX = bone.scaleX + x - bone.data.scaleX
+				bone.scaleY = bone.scaleY + y - bone.data.scaleY
+			else
+				bone.scaleX = x
+				bone.scaleY = y
+			end
+		else
+			local bx = 0
+			local by = 0
+			if direction == MixDirection.out then
+				if blend == MixBlend.setup then
+					bx = bone.data.scaleX
+					by = bone.data.scaleY
+					bone.scaleX = bx + (math_abs(x) * math_signum(bx) - bx) * alpha
+					bone.scaleY = by + (math_abs(y) * math_signum(by) - by) * alpha
+				elseif blend == MixBlend.first or blend == MixBlend.replace then
+					bx = bone.scaleX
+					by = bone.scaleY
+					bone.scaleX = bx + (math_abs(x) * math_signum(bx) - bx) * alpha
+					bone.scaleY = by + (math_abs(y) * math_signum(by) - by) * alpha
+				elseif blend == MixBlend.add then
+					bx = bone.scaleX
+					by = bone.scaleY
+					bone.scaleX = bx + (math_abs(x) * math_signum(bx) - bone.data.scaleX) * alpha
+					bone.scaleY = by + (math_abs(y) * math_signum(by) - bone.data.scaleY) * alpha
+				end
+			else
+				if blend == MixBlend.setup then
+					bx = math_abs(bone.data.scaleX) * math_signum(x)
+					by = math_abs(bone.data.scaleY) * math_signum(y)
+					bone.scaleX = bx + (x - bx) * alpha
+					bone.scaleY = by + (y - by) * alpha
+				elseif blend == MixBlend.first or blend == MixBlend.replace then
+					bx = math_abs(bone.scaleX) * math_signum(x)
+					by = math_abs(bone.scaleY) * math_signum(y)
+					bone.scaleX = bx + (x - bx) * alpha
+					bone.scaleY = by + (y - by) * alpha
+				elseif blend == MixBlend.add then
+					bx = math_signum(x)
+					by = math_signum(y)
+					bone.scaleX = math_abs(bone.scaleX) * bx + (x - math_abs(bone.data.scaleX) * bx) * alpha
+					bone.scaleY = math_abs(bone.scaleY) * by + (y - math_abs(bone.data.scaleY) * by) * alpha
+				end
+			end
+		end
+	end
+
+	return self
+end
+
+Animation.ShearTimeline = {}
+Animation.ShearTimeline.ENTRIES = Animation.TranslateTimeline.ENTRIES
+function Animation.ShearTimeline.new (frameCount)
+	local ENTRIES = Animation.ShearTimeline.ENTRIES
+	local PREV_TIME = -3
+	local PREV_X = -2
+	local PREV_Y = -1
+	local X = 1
+	local Y = 2
+
+	local self = Animation.TranslateTimeline.new(frameCount)
+	self.type = TimelineType.shear
+
+	function self:getPropertyId ()
+		return TimelineType.shear * SHL_24 + self.boneIndex
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local bone = skeleton.bones[self.boneIndex]
+		if not bone.active then return end
+
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				bone.shearX = bone.data.shearX
+				bone.shearY = bone.data.shearY
+			elseif blend == MixBlend.first then
+				bone.shearX = bone.shearX + (bone.data.shearX - bone.shearX) * alpha
+				bone.shearY = bone.shearX + (bone.data.shearY - bone.shearY) * alpha
+			end
+			return
+		end
+
+		local x = 0
+		local y = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- // Time is after last frame.
+			x = frames[zlen(frames) + PREV_X]
+			y = frames[zlen(frames) + PREV_Y]
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			x = frames[frame + PREV_X]
+			y = frames[frame + PREV_Y]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			x = x + (frames[frame + X] - x) * percent
+			y = y + (frames[frame + Y] - y) * percent
+		end
+		if blend == MixBlend.setup then
+			bone.shearX = bone.data.shearX + x * alpha
+			bone.shearY = bone.data.shearY + y * alpha
+		elseif blend == MixBlend.first or blend == MixBlend.replace then
+			bone.shearX = bone.shearX + (bone.data.shearX + x - bone.shearX) * alpha
+			bone.shearY = bone.shearY + (bone.data.shearY + y - bone.shearY) * alpha
+		elseif blend == MixBlend.add then
+			bone.shearX = bone.shearX + x * alpha
+			bone.shearY = bone.shearY + y * alpha
+		end
+	end
+
+	return self
+end
+
+Animation.ColorTimeline = {}
+Animation.ColorTimeline.ENTRIES = 5
+function Animation.ColorTimeline.new (frameCount)
+	local ENTRIES = Animation.ColorTimeline.ENTRIES
+	local PREV_TIME = -5
+	local PREV_R = -4
+	local PREV_G = -3
+	local PREV_B = -2
+	local PREV_A = -1
+	local R = 1
+	local G = 2
+	local B = 3
+	local A = 4
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.slotIndex = -1
+	self.type = TimelineType.color
+
+	function self:getPropertyId ()
+		return TimelineType.color * SHL_24 + self.slotIndex
+	end
+
+	function self:setFrame (frameIndex, time, r, g, b, a)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + R] = r
+		self.frames[frameIndex + G] = g
+		self.frames[frameIndex + B] = b
+		self.frames[frameIndex + A] = a
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+		local slot = skeleton.slots[self.slotIndex]
+		if not slot.bone.active then return end
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				slot.color:setFrom(slot.data.color)
+			elseif blend == MixBlend.first then
+				local color = slot.color
+				local setup = slot.data.color
+				color:add((setup.r - color.r) * alpha, (setup.g - color.g) * alpha, (setup.b - color.b) * alpha,
+						(setup.a - color.a) * alpha)
+			end
+			return
+		end
+
+		local r, g, b, a
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			local i = zlen(frames)
+			r = frames[i + PREV_R]
+			g = frames[i + PREV_G]
+			b = frames[i + PREV_B]
+			a = frames[i + PREV_A]
+		else
+			-- Interpolate between the last frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			r = frames[frame + PREV_R]
+			g = frames[frame + PREV_G]
+			b = frames[frame + PREV_B]
+			a = frames[frame + PREV_A]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math.floor(frame / ENTRIES) - 1,
+					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			r = r + (frames[frame + R] - r) * percent
+			g = g + (frames[frame + G] - g) * percent
+			b = b + (frames[frame + B] - b) * percent
+			a = a + (frames[frame + A] - a) * percent
+		end
+		if alpha == 1 then
+			slot.color:set(r, g, b, a)
+		else
+			local color = slot.color
+			if blend == MixBlend.setup then color:setFrom(slot.data.color) end
+			color:add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha)
+		end
+	end
+
+	return self
+end
+
+Animation.TwoColorTimeline = {}
+Animation.TwoColorTimeline.ENTRIES = 8
+function Animation.TwoColorTimeline.new (frameCount)
+	local ENTRIES = Animation.TwoColorTimeline.ENTRIES
+	local PREV_TIME = -8
+	local PREV_R = -7
+	local PREV_G = -6
+	local PREV_B = -5
+	local PREV_A = -4
+	local PREV_R2 = -3
+	local PREV_G2 = -2
+	local PREV_B2 = -1
+	local R = 1
+	local G = 2
+	local B = 3
+	local A = 4
+	local R2 = 5
+	local G2 = 6
+	local B2 = 7
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.slotIndex = -1
+	self.type = TimelineType.twoColor
+
+	function self:getPropertyId ()
+		return TimelineType.twoColor * SHL_24 + self.slotIndex
+	end
+
+	function self:setFrame (frameIndex, time, r, g, b, a, r2, g2, b2)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + R] = r
+		self.frames[frameIndex + G] = g
+		self.frames[frameIndex + B] = b
+		self.frames[frameIndex + A] = a
+		self.frames[frameIndex + R2] = r2
+		self.frames[frameIndex + G2] = g2
+		self.frames[frameIndex + B2] = b2
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+		local slot = skeleton.slots[self.slotIndex]
+		if not slot.bone.active then return end
+
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				slot.color:setFrom(slot.data.color)
+				slot.darkColor:setFrom(slot.data.darkColor)
+			elseif blend == MixBlend.first then
+				local light = slot.color
+				local dark = slot.darkColor
+				local setupLight = slot.data.color
+				local setupDark = slot.data.darkColor
+				light:add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha,
+					(setupLight.a - light.a) * alpha)
+				dark:add((setupDark.r - dark.r) * alpha, (setupDark.g - dark.g) * alpha, (setupDark.b - dark.b) * alpha, 0)
+			end
+			return
+		end
+
+		local r, g, b, a, r2, g2, b2
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			local i = zlen(frames)
+			r = frames[i + PREV_R]
+			g = frames[i + PREV_G]
+			b = frames[i + PREV_B]
+			a = frames[i + PREV_A]
+			r2 = frames[i + PREV_R2]
+			g2 = frames[i + PREV_G2]
+			b2 = frames[i + PREV_B2]
+		else
+			-- Interpolate between the last frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			r = frames[frame + PREV_R]
+			g = frames[frame + PREV_G]
+			b = frames[frame + PREV_B]
+			a = frames[frame + PREV_A]
+			r2 = frames[frame + PREV_R2]
+			g2 = frames[frame + PREV_G2]
+			b2 = frames[frame + PREV_B2]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math.floor(frame / ENTRIES) - 1,
+					1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			r = r + (frames[frame + R] - r) * percent
+			g = g + (frames[frame + G] - g) * percent
+			b = b + (frames[frame + B] - b) * percent
+			a = a + (frames[frame + A] - a) * percent
+			r2 = r2 + (frames[frame + R2] - r2) * percent
+			g2 = g2 + (frames[frame + G2] - g2) * percent
+			b2 = b2 + (frames[frame + B2] - b2) * percent
+		end
+		if alpha == 1 then
+			slot.color:set(r, g, b, a)
+			slot.darkColor:set(r2, g2, b2, 1)
+		else
+			local light = slot.color
+			local dark = slot.darkColor
+			if blend == MixBlend.setup then
+				light:setFrom(slot.data.color)
+				dark:setFrom(slot.data.darkColor)
+			end
+			light:add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha)
+			dark:add((r2 - dark.r) * alpha, (g2 - dark.g) * alpha, (b2 - dark.b) * alpha, 0)
+		end
+	end
+
+	return self
+end
+
+Animation.AttachmentTimeline = {}
+function Animation.AttachmentTimeline.new (frameCount)
+	local self = {
+		frames = utils.newNumberArrayZero(frameCount), -- time, ...
+		attachmentNames = {},
+		slotIndex = -1,
+		type = TimelineType.attachment
+	}
+
+	function self:getFrameCount ()
+		return zlen(self.frames)
+	end
+
+	function self:setFrame (frameIndex, time, attachmentName)
+		self.frames[frameIndex] = time
+		self.attachmentNames[frameIndex] = attachmentName
+	end
+
+	function self:getPropertyId ()
+		return TimelineType.attachment * SHL_24 + self.slotIndex
+	end
+
+	function self:setAttachment(skeleton, slot, attachmentName)
+		attachmentName = slot.data.attachmentName
+		if not attachmentName then
+			slot:setAttachment(nil)
+		else
+			slot:setAttachment(skeleton:getAttachmentByIndex(self.slotIndex, attachmentName))
+		end
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local slot = skeleton.slots[self.slotIndex]
+		if not slot.bone.active then return end
+		local attachmentName
+		if direction == MixDirection.out then
+			if blend == MixBlend.setup then
+				self:setAttachment(skeleton, slot, slot.data.attachmentName)
+			end
+			return
+		end
+
+		local frames = self.frames
+		if time < frames[0] then
+			if blend == MixBlend.setup or blend == MixBlend.first then
+				self:setAttachment(skeleton, slot, slot.data.attachmentName)
+			end
+			return
+		end
+
+		local frameIndex = 0
+		if time >= frames[zlen(frames) - 1] then
+			frameIndex = zlen(frames) - 1
+		else
+			frameIndex = binarySearch(frames, time, 1) - 1
+		end
+
+		attachmentName = self.attachmentNames[frameIndex]
+		if not attachmentName then
+			slot:setAttachment(nil)
+		else
+			slot:setAttachment(skeleton:getAttachmentByIndex(self.slotIndex, attachmentName))
+		end
+	end
+
+	return self
+end
+
+Animation.DeformTimeline = {}
+function Animation.DeformTimeline.new (frameCount)
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount)
+	self.frameVertices = utils.newNumberArrayZero(frameCount)
+	self.slotIndex = -1
+	self.attachment = nil
+	self.type = TimelineType.deform
+
+	function self:getPropertyId ()
+		return TimelineType.deform * SHL_27 + self.attachment.id + self.slotIndex
+	end
+
+	function self:setFrame (frameIndex, time, vertices)
+		self.frames[frameIndex] = time
+		self.frameVertices[frameIndex] = vertices
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local slot = skeleton.slots[self.slotIndex]
+		if not slot.bone.active then return end
+
+		local slotAttachment = slot.attachment
+		if not slotAttachment then return end
+		if not (slotAttachment.type == AttachmentType.mesh or slotAttachment.type == AttachmentType.linkedmesh or slotAttachment.type == AttachmentType.path or slotAttachment.type == AttachmentType.boundingbox) then return end
+		if slotAttachment.deformAttachment ~= self.attachment then return end
+
+		local frames = self.frames
+		local deformArray = slot.deform
+		if #(deformArray) == 0 then blend = MixBlend.setup end
+
+		local frameVertices = self.frameVertices
+		local vertexCount = #(frameVertices[0])
+
+		if time < frames[0] then
+			local vertexAttachment = slotAttachment
+			if blend == MixBlend.setup then
+				slot.deform = {}
+				return
+			elseif blend == MixBlend.first then
+				if (alpha == 1) then
+					slot.deform = {}
+					return
+				end
+
+				local deform = utils.setArraySize(deformArray, vertexCount)
+				if (vertexAttachment.bones == nil) then
+					local setupVertices = vertexAttachment.vertices
+					local i = 1
+					while i <= vertexCount do
+						deform[i] = deform[i] + (setupVertices[i] - deform[i]) * alpha
+						i = i + 1
+					end
+				else
+					alpha = 1 - alpha
+					local i = 1
+					while i <= vertexCount do
+						deform[i] = deform[i] * alpha
+						i = i + 1
+					end
+				end
+			end
+			return
+		end
+
+		local deform = utils.setArraySize(deformArray, vertexCount)
+		if time >= frames[zlen(frames) - 1] then -- Time is after last frame.
+			local lastVertices = frameVertices[zlen(frames) - 1]
+			if alpha == 1 then
+				if blend == MixBlend.add then
+					local vertexAttachment = slotAttachment
+					if vertexAttachment.bones == nil then
+						-- Unweighted vertex positions, with alpha.
+						local setupVertices = vertexAttachment.vertices
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = deform[i] + lastVertices[i] - setupVertices[i]
+							i = i + 1
+						end
+					else
+						-- Weighted deform offsets, with alpha.
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = deform[i] + lastVertices[i]
+							i = i + 1
+						end
+					end
+				else
+					local i = 1
+					while i <= vertexCount do
+						deform[i] = lastVertices[i]
+						i = i + 1
+					end
+				end
+			else
+				if blend == MixBlend.setup then
+					local vertexAttachment = slotAttachment
+					if vertexAttachment.bones == nil then
+						-- Unweighted vertex positions, with alpha.
+						local setupVertices = vertexAttachment.vertices
+						local i = 1
+						while i <= vertexCount do
+							local setup = setupVertices[i]
+							deform[i] = setup + (lastVertices[i] - setup) * alpha
+							i = i + 1
+						end
+					else
+						-- Weighted deform offsets, with alpha.
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = lastVertices[i] * alpha
+							i = i + 1
+						end
+					end
+				elseif blend == MixBlend.first or blend == MixBlend.replace then
+					local i = 1
+					while i <= vertexCount do
+						deform[i] = deform[i] + (lastVertices[i] - deform[i]) * alpha
+						i = i + 1
+					end
+					local vertexAttachment = slotAttachment
+					if vertexAttachment.bones == nil then
+						local setupVertices = vertexAttachment.vertices
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = deform[i] + (lastVertices[i] - setupVertices[i]) * alpha
+							i = i + 1
+						end
+					else
+						-- Weighted deform offsets, with alpha.
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = deform[i] + lastVertices[i] * alpha
+							i = i + 1
+						end
+					end
+				elseif blend == MixBlend.add then
+					local vertexAttachment = slotAttachment
+					if vertexAttachment.bones == nil then
+						local setupVertices = vertexAttachment.vertices
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = deform[i] + (lastVertices[i] - setupVertices[i]) * alpha
+							i = i + 1
+						end
+					else
+						-- Weighted deform offsets, with alpha.
+						local i = 1
+						while i <= vertexCount do
+							deform[i] = deform[i] + lastVertices[i] * alpha
+							i = i + 1
+						end
+					end
+				end
+			end
+			return
+		end
+
+		-- Interpolate between the previous frame and the current frame.
+		local frame = binarySearch(frames, time, 1)
+		local prevVertices = frameVertices[frame - 1]
+		local nextVertices = frameVertices[frame]
+		local frameTime = frames[frame]
+		local percent = self:getCurvePercent(frame - 1, 1 - (time - frameTime) / (frames[frame - 1] - frameTime))
+
+		if alpha == 1 then
+			if blend == MixBlend.add then
+				local vertexAttachment = slotAttachment
+				if vertexAttachment.bones == nil then
+					-- Unweighted vertex positions, with alpha.
+					local setupVertices = vertexAttachment.vertices
+					local i = 1
+					while i <= vertexCount do
+						local prev = prevVertices[i]
+						deform[i] = deform[i] + prev + (nextVertices[i] - prev) * precent - setupVertices[i]
+						i = i + 1
+					end
+				else
+					-- Weighted deform offsets, with alpha.
+					local i = 1
+					while i <= vertexCount do
+						local prev = prevVertices[i]
+						deform[i] = deform[i] + prev + (nextVertices[i] - prev) * percent
+						i = i + 1
+					end
+				end
+			else
+				local i = 1
+				while i <= vertexCount do
+					local prev = prevVertices[i]
+					deform[i] = prev + (nextVertices[i] - prev) * percent
+					i = i + 1
+				end
+			end
+		else
+			if blend == MixBlend.setup then
+				local vertexAttachment = slotAttachment
+				if vertexAttachment.bones == nil then
+					-- Unweighted vertex positions, with alpha.
+					local setupVertices = vertexAttachment.vertices
+					local i = 1
+					while i <= vertexCount do
+						local prev = prevVertices[i]
+						local setup = setupVertices[i]
+						deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha
+						i = i + 1
+					end
+				else
+					-- Weighted deform offsets, with alpha.
+					local i = 1
+					while i <= vertexCount do
+						local prev = prevVertices[i]
+						deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha
+						i = i + 1
+					end
+				end
+			elseif blend == MixBlend.first or blend == MixBlend.replace then
+				local i = 1
+				while i <= vertexCount do
+					local prev = prevVertices[i]
+					deform[i] = deform[i] + (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha
+					i = i + 1
+				end
+			elseif blend == MixBlend.add then
+				local vertexAttachment = slotAttachment
+				if vertexAttachment.bones == nil then
+					local setupVertices = vertexAttachment.vertices
+					local i = 1
+					while i <= vertexCount do
+						local prev = prevVertices[i]
+						deform[i] = deform[i] + (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha
+						i = i + 1
+					end
+				else
+					-- Weighted deform offsets, with alpha.
+					local i = 1
+					while i <= vertexCount do
+						local prev = prevVertices[i]
+						deform[i] = deform[i] + (prev + (nextVertices[i] - prev) * percent) * alpha
+						i = i + 1
+					end
+				end
+			end
+		end
+	end
+
+	return self
+end
+
+Animation.EventTimeline = {}
+function Animation.EventTimeline.new (frameCount)
+	local self = {
+		frames = utils.newNumberArrayZero(frameCount),
+		events = {},
+		type = TimelineType.event
+	}
+
+	function self:getPropertyId ()
+		return TimelineType.event * SHL_24
+	end
+
+	function self:getFrameCount ()
+		return zlen(self.frames)
+	end
+
+	function self:setFrame (frameIndex, event)
+		self.frames[frameIndex] = event.time
+		self.events[frameIndex] = event
+	end
+
+	-- Fires events for frames > lastTime and <= time.
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		if not firedEvents then return end
+
+		local frames = self.frames
+		local frameCount = zlen(frames)
+
+		if lastTime > time then -- Fire events after last time for looped animations.
+			self:apply(skeleton, lastTime, 999999, firedEvents, alpha, blend, direction)
+			lastTime = -1
+		elseif lastTime >= frames[frameCount - 1] then -- Last time is after last frame.
+			return
+		end
+		if time < frames[0] then return end -- Time is before first frame.
+
+		local frame
+		if lastTime < frames[0] then
+			frame = 0
+		else
+			frame = binarySearch1(frames, lastTime)
+			local frame = frames[frame]
+			while frame > 0 do -- Fire multiple events with the same frame.
+				if frames[frame - 1] ~= frame then break end
+				frame = frame - 1
+			end
+		end
+		local events = self.events
+		while frame < frameCount and time >= frames[frame] do
+			table.insert(firedEvents, events[frame])
+			frame = frame + 1
+		end
+	end
+
+	return self
+end
+
+Animation.DrawOrderTimeline = {}
+function Animation.DrawOrderTimeline.new (frameCount)
+	local self = {
+		frames = utils.newNumberArrayZero(frameCount),
+		drawOrders = {},
+		type = TimelineType.drawOrder
+	}
+
+	function self:getPropertyId ()
+		return TimelineType.drawOrder * SHL_24
+	end
+
+	function self:getFrameCount ()
+		return zlen(self.frames)
+	end
+
+	function self:setFrame (frameIndex, time, drawOrder)
+		self.frames[frameIndex] = time
+		self.drawOrders[frameIndex] = drawOrder
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local drawOrder = skeleton.drawOrder
+		local slots = skeleton.slots
+		if direction == MixDirection.out then
+			if blend == MixBlend.setup then
+				for i,slot in ipairs(slots) do
+					drawOrder[i] = slots[i]
+				end
+			end
+			return
+		end
+
+		local frames = self.frames
+		if time < frames[0] then
+			if blend == MixBlend.setup or blend == MixBlend.first then
+				for i,slot in ipairs(slots) do
+					drawOrder[i] = slots[i]
+				end
+			end
+			return
+		end
+
+		local frame
+		if time >= frames[zlen(frames) - 1] then -- Time is after last frame.
+			frame = zlen(frames) - 1
+		else
+			frame = binarySearch1(frames, time) - 1
+		end
+
+		local drawOrderToSetupIndex = self.drawOrders[frame]
+		if not drawOrderToSetupIndex then
+			for i,slot in ipairs(slots) do
+				drawOrder[i] = slots[i]
+			end
+		else
+			for i,setupIndex in ipairs(drawOrderToSetupIndex) do
+				drawOrder[i] = skeleton.slots[setupIndex]
+			end
+		end
+	end
+
+	return self
+end
+
+Animation.IkConstraintTimeline = {}
+Animation.IkConstraintTimeline.ENTRIES = 6
+function Animation.IkConstraintTimeline.new (frameCount)
+	local ENTRIES = Animation.IkConstraintTimeline.ENTRIES
+	local PREV_TIME = -6
+	local PREV_MIX = -5
+	local PREV_SOFTNESS = -4
+	local PREV_BEND_DIRECTION = -3
+	local PREV_COMPRESS = -2
+	local PREV_STRETCH = -1
+	local MIX = 1
+	local SOFTNESS = 2
+	local BEND_DIRECTION = 3
+	local COMPRESS = 4
+	local STRETCH = 5
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES) -- time, mix, softness, bendDirection, compress, stretch, ...
+	self.ikConstraintIndex = -1
+	self.type = TimelineType.ikConstraint
+
+	function self:getPropertyId ()
+		return TimelineType.ikConstraint * SHL_24 + self.ikConstraintIndex
+	end
+
+	function self:setFrame (frameIndex, time, mix, softness, bendDirection, compress, stretch)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + MIX] = mix
+		self.frames[frameIndex + SOFTNESS] = softness
+		self.frames[frameIndex + BEND_DIRECTION] = bendDirection
+		if (compress) then
+			self.frames[frameIndex + COMPRESS] = 1
+		else
+			self.frames[frameIndex + COMPRESS] = 0
+		end
+		if (stretch) then
+			self.frames[frameIndex + STRETCH] = 1
+		else
+			self.frames[frameIndex + STRETCH] = 0
+		end
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local constraint = skeleton.ikConstraints[self.ikConstraintIndex]
+		if not constraint.active then return end
+		if time < frames[0] then
+			if blend == MixBlend.setup then
+				constraint.mix = constraint.data.mix
+				constraint.softness = constraint.data.softness
+				constraint.bendDirection = constraint.data.bendDirection
+				constraint.compress = constraint.data.compress
+				constraint.stretch = constraint.data.stretch
+			elseif blend == MixBlend.first then
+				constraint.mix = constraint.mix + (constraint.data.mix - constraint.mix) * alpha
+				constraint.softness = constraint.softness + (constraint.data.softness - constraint.softness) * alpha
+				constraint.bendDirection = constraint.data.bendDirection
+				constraint.compress = constraint.data.compress
+				constraint.stretch = constraint.data.stretch
+			end
+			return
+		end
+
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			if blend == MixBlend.setup then
+				constraint.mix = constraint.data.mix + (frames[zlen(frames) + PREV_MIX] - constraint.data.mix) * alpha
+				constraint.softness = constraint.data.softness + (frames[zlen(frames) + PREV_SOFTNESS] - constraint.data.softness) * alpha
+				if direction == MixDirection.out then
+					constraint.bendDirection = constraint.data.bendDirection
+					constraint.compress = constraint.data.compress
+					constraint.stretch = constraint.data.stretch
+				else
+					constraint.bendDirection = math_floor(frames[zlen(frames) + PREV_BEND_DIRECTION])
+					if (math_floor(frames[zlen(frames) + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
+					if (math_floor(frames[zlen(frames) + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
+				end
+			else
+				constraint.mix = constraint.mix + (frames[zlen(frames) + PREV_MIX] - constraint.mix) * alpha
+				constraint.softness = constraint.softness + (frames[zlen(frames) + PREV_SOFTNESS] - constraint.softness) * alpha
+				if direction == MixDirection._in then
+					constraint.bendDirection = math_floor(frames[zlen(frames) + PREV_BEND_DIRECTION])
+					if (math_floor(frames[zlen(frames) + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
+					if (math_floor(frames[zlen(frames) + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
+				end
+			end
+			return
+		end
+
+		-- Interpolate between the previous frame and the current frame.
+		local frame = binarySearch(frames, time, ENTRIES)
+		local mix = frames[frame + PREV_MIX]
+		local softness = frames[frame + PREV_SOFTNESS]
+		local frameTime = frames[frame]
+		local percent = self:getCurvePercent(math.floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+		if blend == MixBlend.setup then
+			constraint.mix = constraint.data.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.data.mix) * alpha
+			constraint.softness = constraint.data.softness + (softness + (frames[frame + SOFTNESS] - softness) * percent - constraint.data.softness) * alpha
+			if direction == MixDirection.out then
+				constraint.bendDirection = constraint.data.bendDirection
+				constraint.compress = constraint.data.compress
+				constraint.stretch = constraint.data.stretch
+			else
+				constraint.bendDirection = math_floor(frames[frame + PREV_BEND_DIRECTION])
+				if (math_floor(frames[frame + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
+				if (math_floor(frames[frame + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
+			end
+		else
+			constraint.mix = constraint.mix + (mix + (frames[frame + MIX] - mix) * percent - constraint.mix) * alpha
+			constraint.softness = constraint.softness + (softness + (frames[frame + SOFTNESS] - softness) * percent - constraint.softness) * alpha
+			if direction == MixDirection._in then
+				constraint.bendDirection = math_floor(frames[frame + PREV_BEND_DIRECTION])
+				if (math_floor(frames[frame + PREV_COMPRESS]) == 1) then constraint.compress = true else constraint.compress = false end
+				if (math_floor(frames[frame + PREV_STRETCH]) == 1) then constraint.stretch = true else constraint.stretch = false end
+			end
+		end
+	end
+
+	return self
+end
+
+Animation.TransformConstraintTimeline = {}
+Animation.TransformConstraintTimeline.ENTRIES = 5
+function Animation.TransformConstraintTimeline.new (frameCount)
+	local ENTRIES = Animation.TransformConstraintTimeline.ENTRIES
+	local PREV_TIME = -5
+	local PREV_ROTATE = -4
+	local PREV_TRANSLATE = -3
+	local PREV_SCALE = -2
+	local PREV_SHEAR = -1
+	local ROTATE = 1
+	local TRANSLATE = 2
+	local SCALE = 3
+	local SHEAR = 4
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.transformConstraintIndex = -1
+	self.type = TimelineType.transformConstraint
+
+	function self:getPropertyId ()
+		return TimelineType.transformConstraint * SHL_24 + self.transformConstraintIndex
+	end
+
+	function self:setFrame (frameIndex, time, rotateMix, translateMix, scaleMix, shearMix)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + ROTATE] = rotateMix
+		self.frames[frameIndex + TRANSLATE] = translateMix
+		self.frames[frameIndex + SCALE] = scaleMix
+		self.frames[frameIndex + SHEAR] = shearMix
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local constraint = skeleton.transformConstraints[self.transformConstraintIndex]
+		if not constraint.active then return end
+
+		if time < frames[0] then
+			local data = constraint.data
+			if blend == MixBlend.setup then
+				constraint.rotateMix = data.rotateMix
+				constraint.translateMix = data.translateMix
+				constraint.scaleMix = data.scaleMix
+				constraint.shearMix = data.shearMix
+			elseif blend == MixBlend.first then
+				constraint.rotateMix = constraint.rotateMix + (data.rotateMix - constraint.rotateMix) * alpha
+				constraint.translateMix = constraint.translateMix + (data.translateMix - constraint.translateMix) * alpha
+				constraint.scaleMix = constraint.scaleMix + (data.scaleMix - constraint.scaleMix) * alpha
+				constraint.shearMix = constraint.shearMix + (data.shearMix - constraint.shearMix) * alpha
+			end
+			return
+		end
+
+		local rotate = 0
+		local translate = 0
+		local scale = 0
+		local shear = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			local i = zlen(frames)
+			rotate = frames[i + PREV_ROTATE]
+			translate = frames[i + PREV_TRANSLATE]
+			scale = frames[i + PREV_SCALE]
+			shear = frames[i + PREV_SHEAR]
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			rotate = frames[frame + PREV_ROTATE]
+			translate = frames[frame + PREV_TRANSLATE]
+			scale = frames[frame + PREV_SCALE]
+			shear = frames[frame + PREV_SHEAR]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			rotate = rotate + (frames[frame + ROTATE] - rotate) * percent
+			translate = translate + (frames[frame + TRANSLATE] - translate) * percent
+			scale = scale + (frames[frame + SCALE] - scale) * percent
+			shear = shear + (frames[frame + SHEAR] - shear) * percent
+		end
+		if blend == MixBlend.setup then
+			local data = constraint.data
+			constraint.rotateMix = data.rotateMix + (rotate - data.rotateMix) * alpha
+			constraint.translateMix = data.translateMix + (translate - data.translateMix) * alpha
+			constraint.scaleMix = data.scaleMix + (scale - data.scaleMix) * alpha
+			constraint.shearMix = data.shearMix + (shear - data.shearMix) * alpha
+		else
+			constraint.rotateMix = constraint.rotateMix + (rotate - constraint.rotateMix) * alpha
+			constraint.translateMix = constraint.translateMix + (translate - constraint.translateMix) * alpha
+			constraint.scaleMix = constraint.scaleMix + (scale - constraint.scaleMix) * alpha
+			constraint.shearMix = constraint.shearMix + (shear - constraint.shearMix) * alpha
+		end
+	end
+
+	return self
+end
+
+Animation.PathConstraintPositionTimeline = {}
+Animation.PathConstraintPositionTimeline.ENTRIES = 2
+function Animation.PathConstraintPositionTimeline.new (frameCount)
+	local ENTRIES = Animation.PathConstraintPositionTimeline.ENTRIES
+	local PREV_TIME = -2
+	local PREV_VALUE = -1
+	local VALUE = 1
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.pathConstraintIndex = -1
+	self.type = TimelineType.pathConstraintPosition
+
+	function self:getPropertyId ()
+		return TimelineType.pathConstraintPosition * SHL_24 + self.pathConstraintIndex
+	end
+
+	function self:setFrame (frameIndex, time, value)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + VALUE] = value
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local constraint = skeleton.pathConstraints[self.pathConstraintIndex]
+		if not constraint.active then return end
+
+		if (time < frames[0]) then
+			if blend == MixBlend.setup then
+				constraint.position = constraint.data.position
+			elseif blend == MixBlend.first then
+				constraint.position = constraint.position + (constraint.data.position - constraint.position) * alpha
+			end
+			return
+		end
+
+		local position = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			position = frames[zlen(frames) + PREV_VALUE]
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			position = frames[frame + PREV_VALUE]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			position = position + (frames[frame + VALUE] - position) * percent
+		end
+		if blend == MixBlend.setup then
+			constraint.position = constraint.data.position + (position - constraint.data.position) * alpha
+		else
+			constraint.position = constraint.position + (position - constraint.position) * alpha
+		end
+	end
+
+	return self
+end
+
+Animation.PathConstraintSpacingTimeline = {}
+Animation.PathConstraintSpacingTimeline.ENTRIES = 2
+function Animation.PathConstraintSpacingTimeline.new (frameCount)
+	local ENTRIES = Animation.PathConstraintSpacingTimeline.ENTRIES
+	local PREV_TIME = -2
+	local PREV_VALUE = -1
+	local VALUE = 1
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.pathConstraintIndex = -1
+	self.type = TimelineType.pathConstraintSpacing
+
+	function self:getPropertyId ()
+		return TimelineType.pathConstraintSpacing * SHL_24 + self.pathConstraintIndex
+	end
+
+	function self:setFrame (frameIndex, time, value)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + VALUE] = value
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local constraint = skeleton.pathConstraints[self.pathConstraintIndex]
+		if not constraint.active then return end
+
+		if (time < frames[0]) then
+			if blend == MixBlend.setup then
+				constraint.spacing = constraint.data.spacing
+			elseif blend == MixBlend.first then
+				constraint.spacing = constraint.spacing + (constraint.data.spacing - constraint.spacing) * alpha
+			end
+			return
+		end
+
+		local spacing = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			spacing = frames[zlen(frames) + PREV_VALUE]
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			spacing = frames[frame + PREV_VALUE]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			spacing = spacing + (frames[frame + VALUE] - spacing) * percent
+		end
+
+		if blend == MixBlend.setup then
+			constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha
+		else
+			constraint.spacing = constraint.spacing + (spacing - constraint.spacing) * alpha
+		end
+	end
+
+	return self
+end
+
+Animation.PathConstraintMixTimeline = {}
+Animation.PathConstraintMixTimeline.ENTRIES = 3
+function Animation.PathConstraintMixTimeline.new (frameCount)
+	local ENTRIES = Animation.PathConstraintMixTimeline.ENTRIES
+	local PREV_TIME = -3
+	local PREV_ROTATE = -2
+	local PREV_TRANSLATE = -1
+	local ROTATE = 1
+	local TRANSLATE = 2
+
+	local self = Animation.CurveTimeline.new(frameCount)
+	self.frames = utils.newNumberArrayZero(frameCount * ENTRIES)
+	self.pathConstraintIndex = -1
+	self.type = TimelineType.pathConstraintMix
+
+	function self:getPropertyId ()
+		return TimelineType.pathConstraintMix * SHL_24 + self.pathConstraintIndex
+	end
+
+	function self:setFrame (frameIndex, time, rotateMix, translateMix)
+		frameIndex = frameIndex * ENTRIES
+		self.frames[frameIndex] = time
+		self.frames[frameIndex + ROTATE] = rotateMix
+		self.frames[frameIndex + TRANSLATE] = translateMix
+	end
+
+	function self:apply (skeleton, lastTime, time, firedEvents, alpha, blend, direction)
+		local frames = self.frames
+
+		local constraint = skeleton.pathConstraints[self.pathConstraintIndex]
+		if not constraint.active then return end
+
+		if (time < frames[0]) then
+			if blend == MixBlend.setup then
+				constraint.rotateMix = constraint.data.rotateMix
+				constraint.translateMix = constraint.data.translateMix
+			elseif blend == MixBlend.first then
+				constraint.rotateMix = constraint.rotateMix + (constraint.data.rotateMix - constraint.rotateMix) * alpha
+				constraint.translateMix = constraint.translateMix + (constraint.data.translateMix - constraint.translateMix) * alpha
+			end
+			return
+		end
+
+		local rotate = 0
+		local translate = 0
+		if time >= frames[zlen(frames) - ENTRIES] then -- Time is after last frame.
+			rotate = frames[zlen(frames) + PREV_ROTATE]
+			translate = frames[zlen(frames) + PREV_TRANSLATE]
+		else
+			-- Interpolate between the previous frame and the current frame.
+			local frame = binarySearch(frames, time, ENTRIES)
+			rotate = frames[frame + PREV_ROTATE]
+			translate = frames[frame + PREV_TRANSLATE]
+			local frameTime = frames[frame]
+			local percent = self:getCurvePercent(math_floor(frame / ENTRIES) - 1,
+				1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime))
+
+			rotate = rotate + (frames[frame + ROTATE] - rotate) * percent
+			translate = translate + (frames[frame + TRANSLATE] - translate) * percent
+		end
+
+		if blend == MixBlend.setup then
+			constraint.rotateMix = constraint.data.rotateMix + (rotate - constraint.data.rotateMix) * alpha
+			constraint.translateMix = constraint.data.translateMix + (translate - constraint.data.translateMix) * alpha
+		else
+			constraint.rotateMix = constraint.rotateMix + (rotate - constraint.rotateMix) * alpha
+			constraint.translateMix = constraint.translateMix + (translate - constraint.translateMix) * alpha
+		end
+	end
+
+	return self
+end
+
+return Animation

+ 19 - 19
spine-lua/spine-lua/AnimationState.lua

@@ -159,7 +159,7 @@ function EventQueue:drain ()
 	end
 	self:clear()
 
-	self.drainDisabled = false;
+	self.drainDisabled = false
 end
 
 function EventQueue:clear ()
@@ -326,7 +326,7 @@ function AnimationState:updateMixingFrom (to, delta)
 
 	from.trackTime = from.trackTime + delta * from.timeScale
 	to.mixTime = to.mixTime + delta
-	return false;
+	return false
 end
 
 function AnimationState:apply (skeleton)
@@ -388,7 +388,7 @@ function AnimationState:apply (skeleton)
 					end
 				end
 				self:queueEvents(current, animationTime)
-				self.events = {};
+				self.events = {}
 				current.nextAnimationLast = animationTime
 				current.nextTrackLast = current.trackTime
 			end
@@ -400,7 +400,7 @@ function AnimationState:apply (skeleton)
 	-- subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or
 	-- the time is before the first key).
 	local setupState = self.unkeyedState + SETUP
-	local slots = skeleton.slots;
+	local slots = skeleton.slots
 	for _, slot in ipairs(slots) do
 		if slot.attachmentState == setupState then
 			local attachmentName = slot.data.attachmentName
@@ -411,7 +411,7 @@ function AnimationState:apply (skeleton)
 			end
 		end
 	end
-	self.unkeyedState = self.unkeyedState + 2; -- Increasing after each use avoids the need to reset attachmentState for every slot.
+	self.unkeyedState = self.unkeyedState + 2 -- Increasing after each use avoids the need to reset attachmentState for every slot.
 
 
 	queue:drain()
@@ -453,11 +453,11 @@ function AnimationState:applyMixingFrom (to, skeleton, blend)
 		local firstFrame = #from.timelinesRotation == 0
 		local timelinesRotation = from.timelinesRotation
 
-		from.totalAlpha = 0;
+		from.totalAlpha = 0
 
 		for i,timeline in ipairs(timelines) do
-			local skipSubsequent = false;
-			local direction = MixDirection.out;
+			local skipSubsequent = false
+			local direction = MixDirection.out
 			local timelineBlend = MixBlend.setup
 			local alpha = 0
 			if timelineMode[i] == SUBSEQUENT then
@@ -498,7 +498,7 @@ function AnimationState:applyMixingFrom (to, skeleton, blend)
 	if (to.mixDuration > 0) then
 		self:queueEvents(from, animationTime)
 	end
-	self.events = {};
+	self.events = {}
 	from.nextAnimationLast = animationTime
 	from.nextTrackLast = from.trackTime
 
@@ -506,20 +506,20 @@ function AnimationState:applyMixingFrom (to, skeleton, blend)
 end
 
 function AnimationState:applyAttachmentTimeline(timeline, skeleton, time, blend, attachments)
-	local slot = skeleton.slots[timeline.slotIndex];
+	local slot = skeleton.slots[timeline.slotIndex]
 	if slot.bone.active == false then return end
 
 	local frames = timeline.frames
 	if time < frames[0] then -- Time is before first frame.
 		if blend == MixBlend.setup or blend == MixBlend.first then
-			self:setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
+			self:setAttachment(skeleton, slot, slot.data.attachmentName, attachments)
 		end
 	else
 		local frameIndex = 0
 		if (time >= frames[zlen(frames) - 1]) then -- Time is after last frame.
-			frameIndex = zlen(frames) - 1;
+			frameIndex = zlen(frames) - 1
 		else
-			frameIndex = Animation.binarySearch(frames, time, 1) - 1;
+			frameIndex = Animation.binarySearch(frames, time, 1) - 1
 		end
 		self:setAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments)
 	end
@@ -665,7 +665,7 @@ function AnimationState:clearTracks ()
 	local queue = self.queue
 	local tracks = self.tracks
 	local oldDrainDisabled = queue.drainDisabled
-	queue.drainDisabled = true;
+	queue.drainDisabled = true
 	local numTracks = getNumTracks(tracks)
 	local i = 0
 	while i <= numTracks do
@@ -673,7 +673,7 @@ function AnimationState:clearTracks ()
 	end
 	tracks = {}
 	queue.drainDisabled = oldDrainDisabled
-	queue:drain();
+	queue:drain()
 end
 
 function AnimationState:clearTrack (trackIndex)
@@ -686,7 +686,7 @@ function AnimationState:clearTrack (trackIndex)
 
 	self:disposeNext(current)
 
-	local entry = current;
+	local entry = current
 	while (true) do
 		local from = entry.mixingFrom
 		if from == nil then break end
@@ -717,7 +717,7 @@ function AnimationState:setCurrent (index, current, interrupt)
 			current.interruptAlpha = current.interruptAlpha * math_min(1, from.mixTime / from.mixDuration)
 		end
 
-		from.timelinesRotation = {};
+		from.timelinesRotation = {}
 	end
 
 	queue:start(current)
@@ -731,7 +731,7 @@ end
 
 function AnimationState:setAnimation (trackIndex, animation, loop)
 	if not animation then error("animation cannot be null.") end
-	local interrupt = true;
+	local interrupt = true
 	local current = self:expandToIndex(trackIndex)
 	local queue = self.queue
 	local tracks = self.tracks
@@ -743,7 +743,7 @@ function AnimationState:setAnimation (trackIndex, animation, loop)
 			queue:_end(current)
 			self:disposeNext(current)
 			current = current.mixingFrom
-			interrupt = false;
+			interrupt = false
 		else
 			self:disposeNext(current)
 		end

+ 14 - 15
spine-lua/spine-lua/Bone.lua

@@ -40,13 +40,12 @@ local math_pi = math.pi
 local TransformMode = require "spine-lua.TransformMode"
 
 function math.sign(x)
-	if x<0 then
+	if x < 0 then
 		return -1
-	elseif x>0 then
+	elseif x > 0 then
 		return 1
-	else
-		return 0
 	end
+	return 0
 end
 
 local math_sign = math.sign
@@ -96,8 +95,8 @@ function Bone:updateWorldTransformWith (x, y, rotation, scaleX, scaleY, shearX,
 	self.ashearY = shearY
 	self.appliedValid = true
 
-	local sx = self.skeleton.scaleX;
-	local sy = self.skeleton.scaleY;
+	local sx = self.skeleton.scaleX
+	local sy = self.skeleton.scaleY
 
 	local parent = self.parent
 	if parent == nil then
@@ -132,7 +131,7 @@ function Bone:updateWorldTransformWith (x, y, rotation, scaleX, scaleY, shearX,
 		self.b = pa * lb + pb * ld
 		self.c = pc * la + pd * lc
 		self.d = pc * lb + pd * ld
-		return;
+		return
 	elseif transformMode == TransformMode.onlyTranslation then
 		local rotationY = rotation + 90 + shearY
 		self.a = math_cos(math_rad(rotation + shearX)) * scaleX
@@ -148,11 +147,11 @@ function Bone:updateWorldTransformWith (x, y, rotation, scaleX, scaleY, shearX,
 			pc = pc / self.skeleton.scaleY
 			pb = pc * s
 			pd = pa * s
-			prx = math_deg(math_atan2(pc, pa));
+			prx = math_deg(math_atan2(pc, pa))
 		else
-			pa = 0;
-			pc = 0;
-			prx = 90 - math_deg(math_atan2(pd, pb));
+			pa = 0
+			pc = 0
+			prx = 90 - math_deg(math_atan2(pd, pb))
 		end
 		local rx = rotation + shearX - prx
 		local ry = rotation + shearY - prx + 90
@@ -181,10 +180,10 @@ function Bone:updateWorldTransformWith (x, y, rotation, scaleX, scaleY, shearX,
 		local r = math_pi / 2 + math_atan2(zc, za)
 		local zb = math_cos(r) * s
 		local zd = math_sin(r) * s
-		local la = math_cos(math_rad(shearX)) * scaleX;
-		local lb = math_cos(math_rad(90 + shearY)) * scaleY;
-		local lc = math_sin(math_rad(shearX)) * scaleX;
-		local ld = math_sin(math_rad(90 + shearY)) * scaleY;
+		local la = math_cos(math_rad(shearX)) * scaleX
+		local lb = math_cos(math_rad(90 + shearY)) * scaleY
+		local lc = math_sin(math_rad(shearX)) * scaleX
+		local ld = math_sin(math_rad(90 + shearY)) * scaleY
 		self.a = za * la + zb * lc
 		self.b = za * lb + zb * ld
 		self.c = zc * la + zd * lc

+ 2 - 1
spine-lua/spine-lua/BoneData.lua

@@ -47,7 +47,8 @@ function BoneData.new (index, name, parent)
 		shearX = 0, shearY = 0,
 		inheritRotation = true,
 		inheritScale = true,
-		skinRequired = false
+		skinRequired = false,
+		color = nil
 	}
 
 	return self

+ 0 - 61
spine-lua/spine-lua/BoundingBoxAttachment.lua

@@ -1,61 +0,0 @@
--------------------------------------------------------------------------------
--- Spine Runtimes License Agreement
--- Last updated January 1, 2020. Replaces all prior versions.
---
--- Copyright (c) 2013-2020, Esoteric Software LLC
---
--- Integration of the Spine Runtimes into software or otherwise creating
--- derivative works of the Spine Runtimes is permitted under the terms and
--- conditions of Section 2 of the Spine Editor License Agreement:
--- http://esotericsoftware.com/spine-editor-license
---
--- Otherwise, it is permitted to integrate the Spine Runtimes into software
--- or otherwise create derivative works of the Spine Runtimes (collectively,
--- "Products"), provided that each user of the Products must obtain their own
--- Spine Editor license and redistribution of the Products in any form must
--- include this license and copyright notice.
---
--- THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
--- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
--- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
--- DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
--- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
--- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
--- BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
--- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
--- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
--- THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
-
-local AttachmentType = require "spine-lua.AttachmentType"
-
-local BoundingBoxAttachment = {}
-function BoundingBoxAttachment.new (name)
-	if not name then error("name cannot be nil", 2) end
-
-	local self = {
-		name = name,
-		type = AttachmentType.boundingbox,
-		vertices = {}
-	}
-
-	function self:computeWorldVertices (x, y, bone, worldVertices)
-		x = x + bone.worldX
-		y = y + bone.worldY
-		local m00 = bone.m00
-		local m01 = bone.m01
-		local m10 = bone.m10
-		local m11 = bone.m11
-		local vertices = self.vertices
-		local count = #vertices
-		for i = 1, count, 2 do
-			local px = vertices[i]
-			local py = vertices[i + 1]
-			worldVertices[i] = px * m00 + py * m01 + x
-			worldVertices[i + 1] = px * m10 + py * m11 + y
-		end
-	end
-
-	return self
-end
-return BoundingBoxAttachment

+ 10 - 10
spine-lua/spine-lua/IkConstraint.lua

@@ -102,12 +102,12 @@ function IkConstraint:apply1 (bone, targetX, targetY, compress, stretch, uniform
 		tx = targetX - bone.worldX
 		ty = targetY - bone.worldY
 	elseif bone.data.transformMode == TransformMode.noRotationOrReflection then
-		local s = math_abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
-		local sa = pa / bone.skeleton.scaleX;
-		local sc = pc / bone.skeleton.scaleY;
-		pb = -sc * s * bone.skeleton.scaleX;
-		pd = sa * s * bone.skeleton.scaleY;
-		rotationIK = rotationIK + math_deg(math_atan2(sc, sa));
+		local s = math_abs(pa * pd - pb * pc) / (pa * pa + pc * pc)
+		local sa = pa / bone.skeleton.scaleX
+		local sc = pc / bone.skeleton.scaleY
+		pb = -sc * s * bone.skeleton.scaleX
+		pd = sa * s * bone.skeleton.scaleY
+		rotationIK = rotationIK + math_deg(math_atan2(sc, sa))
 
 
 		local x = targetX - p.worldX
@@ -255,13 +255,13 @@ function IkConstraint:apply2 (parent, child, targetX, targetY, bendDir, stretch,
 		b = psy * l2
 		local aa = a * a
 		local bb = b * b
-		local ta = math_atan2(ty, tx);
+		local ta = math_atan2(ty, tx)
 		c = bb * l1 * l1 + aa * dd - aa * bb
 		local c1 = -2 * bb * l1
 		local c2 = bb - aa
 		d = c1 * c1 - 4 * c2 * c
 		if d >= 0 then
-			local q = math_sqrt(d);
+			local q = math_sqrt(d)
 			if (c1 < 0) then q = -q end
 			q = -(c1 + q) / 2
 			local r0 = q / c2
@@ -279,7 +279,7 @@ function IkConstraint:apply2 (parent, child, targetX, targetY, bendDir, stretch,
 			local minAngle = math_pi
 			local minX = l1 - a
 			local minDist = minX * minX
-			local minY = 0;
+			local minY = 0
 			local maxAngle = 0
 			local maxX = l1 + a
 			local maxDist = maxX * maxX
@@ -328,7 +328,7 @@ function IkConstraint:apply2 (parent, child, targetX, targetY, bendDir, stretch,
 	elseif a2 < -180 then
 		a2 = a2 + 360
 	end
-	child:updateWorldTransformWith(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
+	child:updateWorldTransformWith(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY)
 end
 
 return IkConstraint

+ 0 - 93
spine-lua/spine-lua/MeshAttachment.lua

@@ -1,93 +0,0 @@
--------------------------------------------------------------------------------
--- Spine Runtimes License Agreement
--- Last updated January 1, 2020. Replaces all prior versions.
---
--- Copyright (c) 2013-2020, Esoteric Software LLC
---
--- Integration of the Spine Runtimes into software or otherwise creating
--- derivative works of the Spine Runtimes is permitted under the terms and
--- conditions of Section 2 of the Spine Editor License Agreement:
--- http://esotericsoftware.com/spine-editor-license
---
--- Otherwise, it is permitted to integrate the Spine Runtimes into software
--- or otherwise create derivative works of the Spine Runtimes (collectively,
--- "Products"), provided that each user of the Products must obtain their own
--- Spine Editor license and redistribution of the Products in any form must
--- include this license and copyright notice.
---
--- THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
--- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
--- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
--- DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
--- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
--- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
--- BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
--- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
--- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
--- THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
-
-local AttachmentType = require "spine-lua.AttachmentType"
-
-local MeshAttachment = {}
-function MeshAttachment.new (name)
-	if not name then error("name cannot be nil", 2) end
-
-	local self = {
-		name = name,
-		type = AttachmentType.mesh,
-		vertices = nil,
-		uvs = nil,
-		regionUVs = nil,
-		triangles = nil,
-		hullLength = 0,
-		r = 1, g = 1, b = 1, a = 1,
-		path = nil,
-		rendererObject = nil,
-		regionU = 0, regionV = 0, regionU2 = 1, regionV2 = 1, regionRotate = false,
-		regionOffsetX = 0, regionOffsetY = 0,
-		regionWidth = 0, regionHeight = 0,
-		regionOriginalWidth = 0, regionOriginalHeight = 0,
-		edges = nil,
-		width = 0, height = 0
-	}
-
-	function self:updateUVs ()
-		local width, height = self.regionU2 - self.regionU, self.regionV2 - self.regionV
-		local n = #self.regionUVs
-		if not self.uvs or #self.uvs ~= n then
-			self.uvs = {}
-		end
-		if self.regionRotate then
-			for i = 1, n, 2 do
-				self.uvs[i] = self.regionU + self.regionUVs[i + 1] * width
-				self.uvs[i + 1] = self.regionV + height - self.regionUVs[i] * height
-			end
-		else
-			for i = 1, n, 2 do
-				self.uvs[i] = self.regionU + self.regionUVs[i] * width
-				self.uvs[i + 1] = self.regionV + self.regionUVs[i + 1] * height
-			end
-		end
-	end
-
-	function self:computeWorldVertices (x, y, slot, worldVertices)
-		local bone = slot.bone
-x,y=slot.bone.skeleton.x,slot.bone.skeleton.y
-		x = x + bone.worldX
-		y = y + bone.worldY
-		local m00, m01, m10, m11 = bone.m00, bone.m01, bone.m10, bone.m11
-		local vertices = self.vertices
-		local verticesCount = #vertices
-		if slot.deform and #slot.deform == verticesCount then vertices = slot.deform end
-		for i = 1, verticesCount, 2 do
-			local vx = vertices[i]
-			local vy = vertices[i + 1]
-			worldVertices[i] = vx * m00 + vy * m01 + x
-			worldVertices[i + 1] = vx * m10 + vy * m11 + y
-		end
-	end
-
-	return self
-end
-return MeshAttachment

+ 15 - 15
spine-lua/spine-lua/PathConstraint.lua

@@ -99,7 +99,7 @@ function PathConstraint:update ()
 	local rotate = rotateMix > 0
 	if not translate and not rotate then return end
 
-	local data = self.data;
+	local data = self.data
 	local percentSpacing = data.spacingMode == PathConstraintData.SpacingMode.percent
 	local rotateMode = data.rotateMode
 	local tangents = rotateMode == PathConstraintData.RotateMode.tangent
@@ -117,7 +117,7 @@ function PathConstraint:update ()
 		local i = 0
 		local n = spacesCount - 1
 		while i < n do
-			local bone = bones[i + 1];
+			local bone = bones[i + 1]
 			local setupLength = bone.data.length
 			if setupLength < PathConstraint.epsilon then
 				if scale then lengths[i + 1] = 0 end
@@ -157,12 +157,12 @@ function PathConstraint:update ()
 	local boneX = positions[1]
 	local boneY = positions[2]
 	local offsetRotation = data.offsetRotation
-	local tip = false;
+	local tip = false
 	if offsetRotation == 0 then
 			tip = rotateMode == PathConstraintData.RotateMode.chain
 	else
-		tip = false;
-		local p = self.target.bone;
+		tip = false
+		local p = self.target.bone
 		if p.a * p.d - p.b * p.c > 0 then
 			offsetRotation = offsetRotation * utils.degRad
 		else
@@ -210,8 +210,8 @@ function PathConstraint:update ()
 				cos = math_cos(r)
 				sin = math_sin(r)
 				local length = bone.data.length
-				boneX = boneX + (length * (cos * a - sin * c) - dx) * rotateMix;
-				boneY = boneY + (length * (sin * a + cos * c) - dy) * rotateMix;
+				boneX = boneX + (length * (cos * a - sin * c) - dx) * rotateMix
+				boneY = boneY + (length * (sin * a + cos * c) - dy) * rotateMix
 			else
 				r = r + offsetRotation
 			end
@@ -249,7 +249,7 @@ function PathConstraint:computeWorldPositions (path, spacesCount, tangents, perc
 	if not path.constantSpeed then
 		local lengths = path.lengths
 		if closed then curveCount = curveCount - 1 else curveCount = curveCount - 2 end
-		local pathLength = lengths[curveCount + 1];
+		local pathLength = lengths[curveCount + 1]
 		if percentPosition then position = position * pathLength end
 		if percentSpacing then
 			i = 1
@@ -258,12 +258,12 @@ function PathConstraint:computeWorldPositions (path, spacesCount, tangents, perc
 				i = i + 1
 			end
 		end
-		world = utils.setArraySize(self.world, 8);
+		world = utils.setArraySize(self.world, 8)
 		i = 0
 		local o = 0
 		local curve = 0
 		while i < spacesCount do
-			local space = spaces[i + 1];
+			local space = spaces[i + 1]
 			position = position + space
 			local p = position
 
@@ -331,14 +331,14 @@ function PathConstraint:computeWorldPositions (path, spacesCount, tangents, perc
 		world[verticesLength - 1 + 1] = world[1 + 1]
 	else
 		curveCount = curveCount - 1
-		verticesLength = verticesLength - 4;
+		verticesLength = verticesLength - 4
 		world = utils.setArraySize(self.world, verticesLength)
 		path:computeWorldVertices(target, 2, verticesLength, world, 0, 2)
 	end
 
 	-- Curve lengths.
 	local curves = utils.setArraySize(self.curves, curveCount)
-	local pathLength = 0;
+	local pathLength = 0
 	local x1 = world[0 + 1]
 	local y1 = world[1 + 1]
 	local cx1 = 0
@@ -392,7 +392,7 @@ function PathConstraint:computeWorldPositions (path, spacesCount, tangents, perc
 	if percentPosition then
 		position = position * pathLength
 	else
-		position = position * pathLength / path.lengths[curveCount];
+		position = position * pathLength / path.lengths[curveCount]
 	end
 	if percentSpacing then
 		i = 1
@@ -496,7 +496,7 @@ function PathConstraint:computeWorldPositions (path, spacesCount, tangents, perc
 						local prev = segments[segment - 1 + 1]
 						p = segment + (p - prev) / (length - prev)
 					end
-					break;
+					break
 				end
 				segment = segment + 1
 			end
@@ -536,7 +536,7 @@ function PathConstraint:addCurvePosition(p, x1, y1, cx1, cy1, cx2, cy2, x2, y2,
 		out[o + 1] = x1
 		out[o + 2] = y1
 		out[o + 3] = math_atan2(cy1 - y1, cx1 - x1)
-		return;
+		return
 	end
 	local tt = p * p
 	local ttt = tt * p

+ 0 - 100
spine-lua/spine-lua/RegionAttachment.lua

@@ -1,100 +0,0 @@
--------------------------------------------------------------------------------
--- Spine Runtimes License Agreement
--- Last updated January 1, 2020. Replaces all prior versions.
---
--- Copyright (c) 2013-2020, Esoteric Software LLC
---
--- Integration of the Spine Runtimes into software or otherwise creating
--- derivative works of the Spine Runtimes is permitted under the terms and
--- conditions of Section 2 of the Spine Editor License Agreement:
--- http://esotericsoftware.com/spine-editor-license
---
--- Otherwise, it is permitted to integrate the Spine Runtimes into software
--- or otherwise create derivative works of the Spine Runtimes (collectively,
--- "Products"), provided that each user of the Products must obtain their own
--- Spine Editor license and redistribution of the Products in any form must
--- include this license and copyright notice.
---
--- THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
--- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
--- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
--- DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
--- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
--- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
--- BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
--- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
--- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
--- THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
-
-local AttachmentType = require "spine-lua.AttachmentType"
-
-local RegionAttachment = {}
-function RegionAttachment.new (name)
-	if not name then error("name cannot be nil", 2) end
-
-	local self = {
-		name = name,
-		type = AttachmentType.region,
-		x = 0, y = 0,
-		rotation = 0,
-		scaleX = 1, scaleY = 1,
-		width = 0, height = 0,
-		offset = {},
-		uvs = {},
-		r = 1, g = 1, b = 1, a = 1,
-		path = nil,
-		rendererObject = nil,
-		regionOffsetX = 0, regionOffsetY = 0,
-		regionWidth = 0, regionHeight = 0,
-		regionOriginalWidth = 0, regionOriginalHeight = 0
-	}
-
-	function self:updateOffset ()
-		local regionScaleX = self.width / self.regionOriginalWidth * self.scaleX
-		local regionScaleY = self.height / self.regionOriginalHeight * self.scaleY
-		local localX = -self.width / 2 * self.scaleX + self.regionOffsetX * regionScaleX
-		local localY = -self.height / 2 * self.scaleY + self.regionOffsetY * regionScaleY
-		local localX2 = localX + self.regionWidth * regionScaleX
-		local localY2 = localY + self.regionHeight * regionScaleY
-		local radians = self.rotation * math.pi / 180
-		local cos = math.cos(radians)
-		local sin = math.sin(radians)
-		local localXCos = localX * cos + self.x
-		local localXSin = localX * sin
-		local localYCos = localY * cos + self.y
-		local localYSin = localY * sin
-		local localX2Cos = localX2 * cos + self.x
-		local localX2Sin = localX2 * sin
-		local localY2Cos = localY2 * cos + self.y
-		local localY2Sin = localY2 * sin
-		local offset = self.offset
-		offset[0] = localXCos - localYSin -- X1
-		offset[1] = localYCos + localXSin -- Y1
-		offset[2] = localXCos - localY2Sin -- X2
-		offset[3] = localY2Cos + localXSin -- Y2
-		offset[4] = localX2Cos - localY2Sin -- X3
-		offset[5] = localY2Cos + localX2Sin -- Y3
-		offset[6] = localX2Cos - localYSin -- X4
-		offset[7] = localYCos + localX2Sin -- Y4
-	end
-
-	function self:computeWorldVertices (x, y, bone, worldVertices)
-		x = x + bone.worldX
-		y = y + bone.worldY
-		local m00, m01, m10, m11 = bone.m00, bone.m01, bone.m10, bone.m11
-		local offset = self.offset
-		local vertices = self.vertices;
-		vertices[0] = offset[0] * m00 + offset[1] * m01 + x
-		vertices[1] = offset[0] * m10 + offset[1] * m11 + y
-		vertices[2] = offset[2] * m00 + offset[3] * m01 + x
-		vertices[3] = offset[2] * m10 + offset[3] * m11 + y
-		vertices[4] = offset[4] * m00 + offset[5] * m01 + x
-		vertices[5] = offset[4] * m10 + offset[5] * m11 + y
-		vertices[6] = offset[6] * m00 + offset[7] * m01 + x
-		vertices[7] = offset[6] * m10 + offset[7] * m11 + y
-	end
-
-	return self
-end
-return RegionAttachment

+ 2 - 2
spine-lua/spine-lua/Skeleton.lua

@@ -460,7 +460,7 @@ function Skeleton:setSkinByReference(newSkin)
 end
 
 function Skeleton:getAttachment (slotName, attachmentName)
-	return self:getAttachmentByIndex(self.data.slotNameIndices[slotName], attachmentName)
+	return self:getAttachmentByIndex(self.data.nameToSlot[slotName].index, attachmentName)
 end
 
 function Skeleton:getAttachmentByIndex (slotIndex, attachmentName)
@@ -520,7 +520,7 @@ end
 function Skeleton:getBounds(offset, size)
 	if not offset then error("offset cannot be null.", 2) end
 	if not size then error("size cannot be null.", 2) end
-	local drawOrder = self.drawOrder;
+	local drawOrder = self.drawOrder
 	local minX = 99999999
 	local minY = 99999999
 	local maxX = -99999999

+ 4 - 4
spine-lua/spine-lua/SkeletonClipping.lua

@@ -118,7 +118,7 @@ function SkeletonClipping:clipTriangles(vertices, uvs, triangles, trianglesLengt
 		local u2 = uvs[vertexOffset]
 		local v2 = uvs[vertexOffset + 1]
 
-		vertexOffset = (triangles[i + 2] - 1) * 2 + 1;
+		vertexOffset = (triangles[i + 2] - 1) * 2 + 1
 		local x3 = vertices[vertexOffset]
 		local y3 = vertices[vertexOffset + 1]
 		local u3 = uvs[vertexOffset]
@@ -135,7 +135,7 @@ function SkeletonClipping:clipTriangles(vertices, uvs, triangles, trianglesLengt
 					local d1 = x3 - x2
 					local d2 = x1 - x3
 					local d4 = y3 - y1
-					local d = 1 / (d0 * d2 + d1 * (y1 - y3));
+					local d = 1 / (d0 * d2 + d1 * (y1 - y3))
 
 					local clipOutputCount = clipOutputLength / 2
 					local clipOutputItems = clipOutput
@@ -193,7 +193,7 @@ function SkeletonClipping:clipTriangles(vertices, uvs, triangles, trianglesLengt
 				clippedTrianglesItems[s] = index
 				clippedTrianglesItems[s + 1] = index + 1
 				clippedTrianglesItems[s + 2] = index + 2
-				index = index + 3;
+				index = index + 3
 				break
 			end
 			p = p + 1
@@ -246,7 +246,7 @@ function SkeletonClipping:clip(x1, y1, x2, y2, x3, y3, clippingArea, output)
 			local inputX2 = inputVertices[ii + 2]
 			local inputY2 = inputVertices[ii + 3]
 			local side2 = deltaX * (inputY2 - edgeY2) - deltaY * (inputX2 - edgeX2) > 0
-			local continue = false;
+			local continue = false
 			if deltaX * (inputY - edgeY2) - deltaY * (inputX - edgeX2) > 0 then
 				if side2 then -- v1 inside, v2 inside
 					table_insert(output, inputX2)

+ 3 - 19
spine-lua/spine-lua/SkeletonData.lua

@@ -37,6 +37,7 @@ function SkeletonData.new ()
 		name,
 		bones = {},
 		slots = {},
+		nameToSlot = {},
 		skins = {},
 		defaultSkin = nil,
 		events = {},
@@ -45,8 +46,7 @@ function SkeletonData.new ()
 		transformConstraints = {},
 		pathConstraints = {},
 		x, y, width, height,
-		version, hash, imagesPath,
-		slotNameIndices = {}
+		version, hash, imagesPath
 	}
 	setmetatable(self, SkeletonData)
 
@@ -71,15 +71,7 @@ end
 
 function SkeletonData:findSlot (slotName)
 	if not slotName then error("slotName cannot be nil.", 2) end
-	for i,slot in ipairs(self.slots) do
-		if slot.name == slotName then return slot end
-	end
-	return nil
-end
-
-function SkeletonData:findSlotIndex (slotName)
-	if not slotName then error("slotName cannot be nil.", 2) end
-	return self.slotNameIndices[slotName] or -1
+	return self.nameToSlot[slotName]
 end
 
 function SkeletonData:findSkin (skinName)
@@ -130,12 +122,4 @@ function SkeletonData:findPathConstraint (constraintName)
 	return nil
 end
 
-function SkeletonData:findPathConstraintIndex (constraintName)
-	if not constraintName then error("constraintName cannot be nil.", 2) end
-	for i,constraint in ipairs(self.pathConstraints) do
-		if constraint.name == constraintName then return i end
-	end
-	return -1
-end
-
 return SkeletonData

+ 537 - 272
spine-lua/spine-lua/SkeletonJson.lua

@@ -65,6 +65,8 @@ function SkeletonJson.new (attachmentLoader)
 	local readAttachment
 	local readAnimation
 	local readCurve
+	local readTimeline1
+	local readTimeline2
 	local getArray
 
 	local getValue = function (map, name, default)
@@ -83,9 +85,6 @@ function SkeletonJson.new (attachmentLoader)
 		if skeletonMap then
 			skeletonData.hash = skeletonMap["hash"]
 			skeletonData.version = skeletonMap["spine"]
-			if ("3.8.75" == skeletonData.version) then
-				error("Unsupported skeleton data, please export with a newer version of Spine.")
-			end
 			skeletonData.x = skeletonMap["x"]
 			skeletonData.y = skeletonMap["y"]
 			skeletonData.width = skeletonMap["width"]
@@ -105,17 +104,25 @@ function SkeletonJson.new (attachmentLoader)
 				if not parent then error("Parent bone not found: " .. parentName) end
 			end
 			local data = BoneData.new(i, boneName, parent)
-			data.length = getValue(boneMap, "length", 0) * scale;
-			data.x = getValue(boneMap, "x", 0) * scale;
-			data.y = getValue(boneMap, "y", 0) * scale;
-			data.rotation = getValue(boneMap, "rotation", 0);
-			data.scaleX = getValue(boneMap, "scaleX", 1);
-			data.scaleY = getValue(boneMap, "scaleY", 1);
-			data.shearX = getValue(boneMap, "shearX", 0);
-			data.shearY = getValue(boneMap, "shearY", 0);
+			data.length = getValue(boneMap, "length", 0) * scale
+			data.x = getValue(boneMap, "x", 0) * scale
+			data.y = getValue(boneMap, "y", 0) * scale
+			data.rotation = getValue(boneMap, "rotation", 0)
+			data.scaleX = getValue(boneMap, "scaleX", 1)
+			data.scaleY = getValue(boneMap, "scaleY", 1)
+			data.shearX = getValue(boneMap, "shearX", 0)
+			data.shearY = getValue(boneMap, "shearY", 0)
 			data.transformMode = TransformMode[getValue(boneMap, "transform", "normal")]
 			data.skinRequired = getValue(boneMap, "skin", false)
 
+			local color = boneMap["color"]
+			if color then
+				data.color = Color.newWith(tonumber(color:sub(1, 2), 16) / 255,
+					tonumber(color:sub(3, 4), 16) / 255,
+					tonumber(color:sub(5, 6), 16) / 255,
+					tonumber(color:sub(7, 8), 16) / 255)
+			end
+
 			table_insert(skeletonData.bones, data)
 		end
 
@@ -138,18 +145,17 @@ function SkeletonJson.new (attachmentLoader)
 
 				local dark = slotMap["dark"]
 				if dark then
-					data.darkColor = Color.newWith(1, 1, 1, 1)
-					data.darkColor:set(tonumber(dark:sub(1, 2), 16) / 255,
+					data.darkColor = Color.newWith(
+						tonumber(dark:sub(1, 2), 16) / 255,
 						tonumber(dark:sub(3, 4), 16) / 255,
-						tonumber(dark:sub(5, 6), 16) / 255,
-						0)
+						tonumber(dark:sub(5, 6), 16) / 255, 0)
 				end
 
 				data.attachmentName = getValue(slotMap, "attachment", nil)
 				data.blendMode = BlendMode[getValue(slotMap, "blend", "normal")]
 
 				table_insert(skeletonData.slots, data)
-				skeletonData.slotNameIndices[data.name] = #skeletonData.slots
+				skeletonData.nameToSlot[data.name] = data
 			end
 		end
 
@@ -204,17 +210,19 @@ function SkeletonJson.new (attachmentLoader)
 
 				data.local_ = getValue(constraintMap, "local", false)
 				data.relative = getValue(constraintMap, "relative", false)
-				data.offsetRotation = getValue(constraintMap, "rotation", 0);
-				data.offsetX = getValue(constraintMap, "x", 0) * scale;
-				data.offsetY = getValue(constraintMap, "y", 0) * scale;
-				data.offsetScaleX = getValue(constraintMap, "scaleX", 0);
-				data.offsetScaleY = getValue(constraintMap, "scaleY", 0);
-				data.offsetShearY = getValue(constraintMap, "shearY", 0);
-
-				data.rotateMix = getValue(constraintMap, "rotateMix", 1);
-				data.translateMix = getValue(constraintMap, "translateMix", 1);
-				data.scaleMix = getValue(constraintMap, "scaleMix", 1);
-				data.shearMix = getValue(constraintMap, "shearMix", 1);
+				data.offsetRotation = getValue(constraintMap, "rotation", 0)
+				data.offsetX = getValue(constraintMap, "x", 0) * scale
+				data.offsetY = getValue(constraintMap, "y", 0) * scale
+				data.offsetScaleX = getValue(constraintMap, "scaleX", 0)
+				data.offsetScaleY = getValue(constraintMap, "scaleY", 0)
+				data.offsetShearY = getValue(constraintMap, "shearY", 0)
+
+				data.mixRotate = getValue(constraintMap, "rotateMix", 1)
+				data.mixX = getValue(constraintMap, "mixX", 1)
+				data.mixY = getValue(constraintMap, "mixY", data.mixX)
+				data.mixScaleX = getValue(constraintMap, "mixScaleX", 1)
+				data.mixScaleY = getValue(constraintMap, "mixScaleY", data.mixScaleX)
+				data.mixShearY = getValue(constraintMap, "mixShearY", 1)
 
 				table_insert(skeletonData.transformConstraints, data)
 			end
@@ -222,8 +230,8 @@ function SkeletonJson.new (attachmentLoader)
 
 		-- Path constraints
 		if root["path"] then
-			for _,constraintMap in ipairs(root.path) do
-				local data = PathConstraintData.new(constraintMap.name);
+			for _,constraintMap in ipairs(root["path"]) do
+				local data = PathConstraintData.new(constraintMap.name)
 				data.order = getValue(constraintMap, "order", 0)
 				data.skinRequired = getValue(constraintMap, "skin", false)
 
@@ -233,20 +241,21 @@ function SkeletonJson.new (attachmentLoader)
 					table_insert(data.bones, bone)
 				end
 
-				local targetName = constraintMap.target;
+				local targetName = constraintMap.target
 				data.target = skeletonData:findSlot(targetName)
 				if data.target == nil then error("Path target slot not found: " .. targetName, 2) end
 
 				data.positionMode = PathConstraintData.PositionMode[getValue(constraintMap, "positionMode", "percent"):lower()]
 				data.spacingMode = PathConstraintData.SpacingMode[getValue(constraintMap, "spacingMode", "length"):lower()]
 				data.rotateMode = PathConstraintData.RotateMode[getValue(constraintMap, "rotateMode", "tangent"):lower()]
-				data.offsetRotation = getValue(constraintMap, "rotation", 0);
-				data.position = getValue(constraintMap, "position", 0);
+				data.offsetRotation = getValue(constraintMap, "rotation", 0)
+				data.position = getValue(constraintMap, "position", 0)
 				if data.positionMode == PathConstraintData.PositionMode.fixed then data.position = data.position * scale end
-				data.spacing = getValue(constraintMap, "spacing", 0);
+				data.spacing = getValue(constraintMap, "spacing", 0)
 				if data.spacingMode == PathConstraintData.SpacingMode.length or data.spacingMode == PathConstraintData.SpacingMode.fixed then data.spacing = data.spacing * scale end
-				data.rotateMix = getValue(constraintMap, "rotateMix", 1);
-				data.translateMix = getValue(constraintMap, "translateMix", 1);
+				data.mixRotate = getValue(constraintMap, "mixRotate", 1)
+				data.mixX = getValue(constraintMap, "mixX", 1)
+				data.mixY = getValue(constraintMap, "mixY", data.mixX)
 
 				table_insert(skeletonData.pathConstraints, data)
 			end
@@ -290,7 +299,7 @@ function SkeletonJson.new (attachmentLoader)
 				end
 				
 				for slotName,slotMap in pairs(skinMap.attachments) do
-					local slotIndex = skeletonData.slotNameIndices[slotName]
+					local slotIndex = skeletonData:findSlot(slotName).index
 					for attachmentName,attachmentMap in pairs(slotMap) do
 						local attachment = readAttachment(attachmentMap, skin, slotIndex, attachmentName, skeletonData)
 						if attachment then
@@ -360,11 +369,11 @@ function SkeletonJson.new (attachmentLoader)
 			region.path = path
 			region.x = getValue(map, "x", 0) * scale
 			region.y = getValue(map, "y", 0) * scale
-			region.scaleX = getValue(map, "scaleX", 1);
-			region.scaleY = getValue(map, "scaleY", 1);
-			region.rotation = getValue(map, "rotation", 0);
-			region.width = map.width * scale;
-			region.height = map.height * scale;
+			region.scaleX = getValue(map, "scaleX", 1)
+			region.scaleY = getValue(map, "scaleY", 1)
+			region.rotation = getValue(map, "rotation", 0)
+			region.width = map.width * scale
+			region.height = map.height * scale
 
 			local color = map["color"]
 			if color then
@@ -453,7 +462,7 @@ function SkeletonJson.new (attachmentLoader)
 					tonumber(color:sub(5, 6), 16) / 255,
 					tonumber(color:sub(7, 8), 16) / 255)
 			end
-			return path;
+			return path
 
 		elseif type == AttachmentType.point then
 			local point = self.attachmentLoader:newPointAttachment(skin, name)
@@ -536,124 +545,239 @@ function SkeletonJson.new (attachmentLoader)
 
 	readAnimation = function (map, name, skeletonData)
 		local timelines = {}
-		local duration = 0
 		local scale = self.scale
 
-		-- Slot timelines
+		-- Slot timelines.
 		local slotsMap = map["slots"]
 		if slotsMap then
-			for slotName,timelineMap in pairs(slotsMap) do
-				local slotIndex = skeletonData.slotNameIndices[slotName]
-
-				for timelineName,values in pairs(timelineMap) do
-					if timelineName == "color" then
-						local timeline = Animation.ColorTimeline.new(#values)
-						timeline.slotIndex = slotIndex
-
-						local frameIndex = 0
-						for _,valueMap in ipairs(values) do
-							local color = valueMap["color"]
-							timeline:setFrame(
-								frameIndex, getValue(valueMap, "time", 0),
-								tonumber(color:sub(1, 2), 16) / 255,
-								tonumber(color:sub(3, 4), 16) / 255,
-								tonumber(color:sub(5, 6), 16) / 255,
-								tonumber(color:sub(7, 8), 16) / 255
-							)
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
+			for slotName,slotMap in pairs(slotsMap) do
+				local slotIndex = skeletonData:findSlot(slotName).index
+				for timelineName,timelineMap in pairs(slotMap) do
+					if not timelineMap then
+					elseif timelineName == "attachment" then
+						local timeline = Animation.AttachmentTimeline.new(#timelineMap, slotIndex)
+						for i,keyMap in ipairs(timelineMap) do
+							timeline:setFrame(i + 1, getValue(keyMap, "time", 0), keyMap["name"])
 						end
 						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.ColorTimeline.ENTRIES])
-					elseif timelineName == "twoColor" then
-						local timeline = Animation.TwoColorTimeline.new(#values)
-						timeline.slotIndex = slotIndex
-
-						local frameIndex = 0
-						for _,valueMap in ipairs(values) do
-							local light = valueMap["light"]
-							local dark = valueMap["dark"]
-							timeline:setFrame(
-								frameIndex, getValue(valueMap, "time", 0),
-								tonumber(light:sub(1, 2), 16) / 255,
-								tonumber(light:sub(3, 4), 16) / 255,
-								tonumber(light:sub(5, 6), 16) / 255,
-								tonumber(light:sub(7, 8), 16) / 255,
-								tonumber(dark:sub(1, 2), 16) / 255,
-								tonumber(dark:sub(3, 4), 16) / 255,
-								tonumber(dark:sub(5, 6), 16) / 255
-							)
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
+					elseif timelineName == "rgba" then
+						local timeline = Animation.RGBATimeline.new(#timelineMap, #timelineMap * 4, slotIndex)
+						local keyMap = timelineMap[1]
+						local time = getValue(keyMap, "time", 0)
+						local color = keyMap["color"]
+						local r = tonumber(color:sub(1, 2), 16) / 255
+						local g = tonumber(color:sub(3, 4), 16) / 255
+						local b = tonumber(color:sub(5, 6), 16) / 255
+						local a = tonumber(color:sub(7, 8), 16) / 255
+						local bezier = 0
+						for i,keyMap in ipairs(timelineMap) do
+							local frame = i - 1
+							timeline:setFrame(frame, time, r, g, b, a)
+							local nextMap = timelineMap[i + 1]
+							if not nextMap then
+								timeline:shrink(bezier)
+								break
+							end
+							local time2 = getValue(nextMap, "time", 0)
+							color = nextMap["color"]
+							local nr = tonumber(color:sub(1, 2), 16) / 255
+							local ng = tonumber(color:sub(3, 4), 16) / 255
+							local nb = tonumber(color:sub(5, 6), 16) / 255
+							local na = tonumber(color:sub(7, 8), 16) / 255
+							local curve = keyMap.curve
+							if curve then
+								bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1)
+							end
+							time = time2
+							r = nr
+							g = ng
+							b = nb
+							a = na
 						end
 						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.TwoColorTimeline.ENTRIES])
-					elseif timelineName == "attachment" then
-						local timeline = Animation.AttachmentTimeline.new(#values)
-						timeline.slotIndex = slotIndex
-
-						local frameIndex = 0
-						for _,valueMap in ipairs(values) do
-							local attachmentName = valueMap["name"]
-							timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), attachmentName)
-							frameIndex = frameIndex + 1
+					elseif timelineName == "rgb" then
+						local timeline = Animation.RGBTimeline.new(#timelineMap, #timelineMap * 3, slotIndex)
+						local keyMap = timelineMap[1]
+						local time = getValue(keyMap, "time", 0)
+						local color = keyMap["color"]
+						local r = tonumber(color:sub(1, 2), 16) / 255
+						local g = tonumber(color:sub(3, 4), 16) / 255
+						local b = tonumber(color:sub(5, 6), 16) / 255
+						local bezier = 0
+						for i,keyMap in ipairs(timelineMap) do
+							local frame = i - 1
+							timeline:setFrame(frame, time, r, g, b)
+							local nextMap = timelineMap[i + 1]
+							if not nextMap then
+								timeline:shrink(bezier)
+								break
+							end
+							local time2 = getValue(nextMap, "time", 0)
+							color = nextMap["color"]
+							local nr = tonumber(color:sub(1, 2), 16) / 255
+							local ng = tonumber(color:sub(3, 4), 16) / 255
+							local nb = tonumber(color:sub(5, 6), 16) / 255
+							local curve = keyMap.curve
+							if curve then
+								bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1)
+							end
+							time = time2
+							r = nr
+							g = ng
+							b = nb
+						end
+						table_insert(timelines, timeline)
+					elseif timelineName == "alpha" then
+						table_insert(timelines, readTimeline1(timelineMap, Animation.AlphaTimeline.new(#timelineMap, #timelineMap, slotIndex), 0, 1))
+					elseif timelineName == "rgba2" then
+						local timeline = Animation.RGBA2Timeline.new(#timelineMap, #timelineMap * 7, slotIndex)
+						local keyMap = timelineMap[1]
+						local time = getValue(keyMap, "time", 0)
+						local color = keyMap["light"]
+						local r = tonumber(color:sub(1, 2), 16) / 255
+						local g = tonumber(color:sub(3, 4), 16) / 255
+						local b = tonumber(color:sub(5, 6), 16) / 255
+						local a = tonumber(color:sub(7, 8), 16) / 255
+						color = keyMap["dark"]
+						local r2 = tonumber(color:sub(1, 2), 16) / 255
+						local g2 = tonumber(color:sub(3, 4), 16) / 255
+						local b2 = tonumber(color:sub(5, 6), 16) / 255
+						local bezier = 0
+						for i,keyMap in ipairs(timelineMap) do
+							local frame = i - 1
+							timeline:setFrame(frame, time, r, g, b, a, r2, g2, b2)
+							local nextMap = timelineMap[i + 1]
+							if not nextMap then
+								timeline:shrink(bezier)
+								break
+							end
+							local time2 = getValue(nextMap, "time", 0)
+							color = nextMap["light"]
+							local nr = tonumber(color:sub(1, 2), 16) / 255
+							local ng = tonumber(color:sub(3, 4), 16) / 255
+							local nb = tonumber(color:sub(5, 6), 16) / 255
+							local na = tonumber(color:sub(7, 8), 16) / 255
+							color = nextMap["dark"]
+							local nr2 = tonumber(color:sub(1, 2), 16) / 255
+							local ng2 = tonumber(color:sub(3, 4), 16) / 255
+							local nb2 = tonumber(color:sub(5, 6), 16) / 255
+							local curve = keyMap.curve
+							if curve then
+								bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, a, na, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, r2, nr2, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, g2, ng2, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 6, time, time2, b2, nb2, 1)
+							end
+							time = time2
+							r = nr
+							g = ng
+							b = nb
+							a = na
+							r2 = nr2
+							g2 = ng2
+							b2 = nb2
+						end
+						table_insert(timelines, timeline)
+					elseif timelineName == "rgb2" then
+						local timeline = Animation.RGB2Timeline.new(#timelineMap, #timelineMap * 6, slotIndex)
+						local keyMap = timelineMap[1]
+						local time = getValue(keyMap, "time", 0)
+						local color = keyMap["light"]
+						local r = tonumber(color:sub(1, 2), 16) / 255
+						local g = tonumber(color:sub(3, 4), 16) / 255
+						local b = tonumber(color:sub(5, 6), 16) / 255
+						color = keyMap["dark"]
+						local r2 = tonumber(color:sub(1, 2), 16) / 255
+						local g2 = tonumber(color:sub(3, 4), 16) / 255
+						local b2 = tonumber(color:sub(5, 6), 16) / 255
+						local bezier = 0
+						for i,keyMap in ipairs(timelineMap) do
+							local frame = i - 1
+							timeline:setFrame(frame, time, r, g, b, r2, g2, b2)
+							local nextMap = timelineMap[i + 1]
+							if not nextMap then
+								timeline:shrink(bezier)
+								break
+							end
+							local time2 = getValue(nextMap, "time", 0)
+							color = nextMap["light"]
+							local nr = tonumber(color:sub(1, 2), 16) / 255
+							local ng = tonumber(color:sub(3, 4), 16) / 255
+							local nb = tonumber(color:sub(5, 6), 16) / 255
+							color = nextMap["dark"]
+							local nr2 = tonumber(color:sub(1, 2), 16) / 255
+							local ng2 = tonumber(color:sub(3, 4), 16) / 255
+							local nb2 = tonumber(color:sub(5, 6), 16) / 255
+							local curve = keyMap.curve
+							if curve then
+								bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, r, nr, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, g, ng, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, b, nb, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, r2, nr2, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, g2, ng2, 1)
+								bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, b2, nb2, 1)
+							end
+							time = time2
+							r = nr
+							g = ng
+							b = nb
+							r2 = nr2
+							g2 = ng2
+							b2 = nb2
 						end
 						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[timeline:getFrameCount() - 1])
-
 					else
-						error("Invalid frame type for a slot: " .. timelineName .. " (" .. slotName .. ")")
+						error("Invalid timeline type for a slot: " .. timelineName .. " (" .. slotName .. ")")
 					end
 				end
 			end
 		end
 
-		-- Bone timelines
+		-- Bone timelines.
 		local bonesMap = map["bones"]
 		if bonesMap then
-			for boneName,timelineMap in pairs(bonesMap) do
+			for boneName,boneMap in pairs(bonesMap) do
 				local boneIndex = skeletonData:findBoneIndex(boneName)
 				if boneIndex == -1 then error("Bone not found: " .. boneName) end
-
-				for timelineName,values in pairs(timelineMap) do
-					if timelineName == "rotate" then
-						local timeline = Animation.RotateTimeline.new(#values)
-						timeline.boneIndex = boneIndex
-
-						local frameIndex = 0
-						for _,valueMap in ipairs(values) do
-							timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), getValue(valueMap, "angle", 0))
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
-						end
-						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.RotateTimeline.ENTRIES])
-
-					elseif timelineName == "translate" or timelineName == "scale" or timelineName == "shear" then
-						local timeline
-						local timelineScale = 1
-						local defaultValue = 0
-						if timelineName == "scale" then
-							timeline = Animation.ScaleTimeline.new(#values)
-							defaultValue = 1
-						elseif timelineName == "shear" then
-							timeline = Animation.ShearTimeline.new(#values)
-						else
-							timeline = Animation.TranslateTimeline.new(#values)
-							timelineScale = self.scale
-						end
-						timeline.boneIndex = boneIndex
-
-						local frameIndex = 0
-						for _,valueMap in ipairs(values) do
-							local x = getValue(valueMap, "x", defaultValue) * timelineScale
-							local y = getValue(valueMap, "y", defaultValue) * timelineScale
-							timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), x, y)
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
-						end
-						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.TranslateTimeline.ENTRIES])
+				for timelineName,timelineMap in pairs(boneMap) do
+					if not timelineMap then
+					elseif timelineName == "rotate" then
+						table_insert(timelines, readTimeline1(timelineMap, Animation.RotateTimeline.new(#timelineMap, #timelineMap, boneIndex), 0, 1))
+					elseif timelineName == "translate" then
+						local timeline = Animation.TranslateTimeline.new(#timelineMap, #timelineMap * 2, boneIndex)
+						table_insert(timelines, readTimeline2(timelineMap, timeline, "x", "y", 0, scale))
+					elseif timelineName == "translatex" then
+						local timeline = Animation.TranslateXTimeline.new(#timelineMap, #timelineMap, boneIndex)
+						table_insert(timelines, readTimeline1(timelineMap, timeline, 0, scale))
+					elseif timelineName == "translatey" then
+						local timeline = Animation.TranslateYTimeline.new(#timelineMap, #timelineMap, boneIndex)
+						table_insert(timelines, readTimeline1(timelineMap, timeline, 0, scale))
+					elseif timelineName == "scale" then
+						local timeline = Animation.ScaleTimeline.new(#timelineMap, #timelineMap * 2, boneIndex)
+						table_insert(timelines, readTimeline2(timelineMap, "x", "y", 1, 1))
+					elseif timelineName == "scalex" then
+						local timeline = Animation.ScaleXTimeline.new(#timelineMap, #timelineMap, boneIndex)
+						table_insert(timelines, readTimeline1(timelineMap, timeline, 1, 1))
+					elseif timelineName == "scaley" then
+						local timeline = Animation.ScaleYTimeline.new(#timelineMap, #timelineMap, boneIndex)
+						table_insert(timelines, readTimeline1(timelineMap, timeline, 1, 1))
+					elseif timelineName == "shear" then
+						local timeline = Animation.ShearTimeline.new(#timelineMap, #timelineMap * 2, boneIndex)
+						table_insert(timelines, readTimeline2(timelineMap, "x", "y", 0, 1))
+					elseif timelineName == "shearx" then
+						local timeline = Animation.ShearXTimeline.new(#timelineMap, #timelineMap, boneIndex)
+						table_insert(timelines, readTimeline1(timelineMap, timeline, 0, 1))
+					elseif timelineName == "sheary" then
+						local timeline = Animation.ShearYTimeline.new(#timelineMap, #timelineMap, boneIndex)
+						table_insert(timelines, readTimeline1(timelineMap, timeline, 0, 1))
 					else
 						error("Invalid timeline type for a bone: " .. timelineName .. " (" .. boneName .. ")")
 					end
@@ -664,96 +788,169 @@ function SkeletonJson.new (attachmentLoader)
 		-- IK timelines.
 		local ik = map["ik"]
 		if ik then
-			for ikConstraintName,values in pairs(ik) do
-				local ikConstraint = skeletonData:findIkConstraint(ikConstraintName)
-				local timeline = Animation.IkConstraintTimeline.new(#values)
-				for i,other in pairs(skeletonData.ikConstraints) do
-					if other == ikConstraint then
-						timeline.ikConstraintIndex = i
-						break
+			for constraintName,timelineMap in pairs(ik) do
+				local keyMap = timelineMap[1]
+				if keyMap then
+					local constraintIndex = -1
+					for i,other in pairs(skeletonData.ikConstraints) do
+						if other.name == constraintName then
+							constraintIndex = i
+							break
+						end
+					end
+					local timeline = Animation.IkConstraintTimeline.new(#timelineMap, #timelineMap * 2, constraintIndex)
+					local time = getValue(keyMap, "time", 0)
+					local mix = getValue(keyMap, "mix", 1)
+					local softness = getValue(keyMap, "softness", 0) * scale
+					local bezier = 0
+					for i,keyMap in ipairs(timelineMap) do
+						local frame = i - 1
+						local bendPositive = 1
+						local compress = false
+						local stretch = false
+						if keyMap["bendPositive"] == false then bendPositive = -1 end
+						if keyMap["compress"] ~= nil then compress = keyMap["compress"] end
+						if keyMap["stretch"] ~= nil then stretch = keyMap["stretch"] end
+						timeline:setFrame(frame, time, mix, softness, bendPositive, compress, stretch)
+						local nextMap = timelineMap[i + 1]
+						if not nextMap then
+							timeline:shrink(bezier)
+							break
+						end
+						local time2 = getValue(nextMap, "time", 0)
+						color = nextMap["color"]
+						local mix2 = getValue(nextMap, "mix", 1)
+						local softness2 = getValue(nextMap, "softness", 0) * scale
+						local curve = keyMap.curve
+						if curve then
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mix, mix2, 1)
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, softness, softness2, scale)
+						end
+						time = time2
+						mix = mix2
+						softness = softness2
 					end
-				end
-				local frameIndex = 0
-				for _,valueMap in ipairs(values) do
-					local mix = 1
-					local softness = 0
-					if valueMap["mix"] ~= nil then mix = valueMap["mix"] end
-					if valueMap["softness"] ~= nil then softness = valueMap["softness"] * scale end
-					local bendPositive = 1
-					if valueMap["bendPositive"] == false then bendPositive = -1 end
-					local stretch = false
-					if valueMap["stretch"] ~= nil then stretch = valueMap["stretch"] end
-					local compress = false
-					if valueMap["compress"] ~= nil then compress = valueMap["compress"] end
-					timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), mix, softness, bendPositive, compress, stretch)
-					readCurve(valueMap, timeline, frameIndex)
-					frameIndex = frameIndex + 1
 				end
 				table_insert(timelines, timeline)
-				duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.IkConstraintTimeline.ENTRIES])
 			end
 		end
 
 		-- Transform constraint timelines.
 		local transform = map["transform"]
 		if transform then
-			for constraintName, values in pairs(transform) do
-				local constraint = skeletonData:findTransformConstraint(constraintName)
-				local timeline = Animation.TransformConstraintTimeline.new(#values)
-				for i,other in pairs(skeletonData.transformConstraints) do
-					if other == constraint then
-						timeline.transformConstraintIndex = i
-						break
+			for constraintName, timelineMap in pairs(transform) do
+				local keyMap = timelineMap[1]
+				if keyMap then
+					local constraintIndex = -1
+					for i,other in pairs(skeletonData.transformConstraints) do
+						if other.name == constraintName then
+							constraintIndex = i
+							break
+						end
 					end
+					local timeline = Animation.TransformConstraintTimeline.new(#timelineMap, #timelineMap * 4, constraintIndex)
+					local time = getValue(keyMap, "time", 0)
+					local mixRotate = getValue(keyMap, "mixRotate", 0)
+					local mixX = getValue(keyMap, "mixX", 1)
+					local mixY = getValue(keyMap, "mixY", mixX)
+					local mixScaleX = getValue(keyMap, "mixScaleX", 1)
+					local mixScaleY = getValue(keyMap, "mixScaleY", mixScaleX)
+					local mixShearY = getValue(keyMap, "mixShearY", 1)
+					local bezier = 0
+					for i,keyMap in ipairs(timelineMap) do
+						local frame = i - 1
+						timeline:setFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY)
+						local nextMap = timelineMap[frame + 1]
+						if not nextMap then
+							timeline:shrink(bezier)
+							break
+						end
+						local time2 = getValue(nextMap, "time", 0)
+						local mixRotate2 = getValue(nextMap, "mixRotate", 1)
+						local mixX2 = getValue(nextMap, "mixX", 1)
+						local mixY2 = getValue(nextMap, "mixY", mixX2)
+						local mixScaleX2 = getValue(nextMap, "mixScaleX", 1)
+						local mixScaleY2 = getValue(nextMap, "mixScaleY", mixScaleX2)
+						local mixShearY2 = getValue(nextMap, "mixShearY", 1)
+						local curve = keyMap.curve
+						if curve then
+							bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1)
+							bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1)
+							bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1)
+							bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, mixScaleX, mixScaleX2, 1)
+							bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, mixScaleY, mixScaleY2, 1)
+							bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, mixShearY, mixShearY2, 1)
+						end
+						time = time2
+						mixRotate = mixRotate2
+						mixX = mixX2
+						mixY = mixY2
+						mixScaleX = mixScaleX2
+						mixScaleY = mixScaleY2
+						mixScaleX = mixScaleX2
+					end
+					table_insert(timelines, timeline)
 				end
-				local frameIndex = 0
-				for _,valueMap in ipairs(values) do
-					timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), getValue(valueMap, "rotateMix", 1), getValue(valueMap, "translateMix", 1), getValue(valueMap, "scaleMix", 1), getValue(valueMap, "shearMix", 1))
-					readCurve(valueMap, timeline, frameIndex)
-					frameIndex = frameIndex + 1
-				end
-				table_insert(timelines, timeline)
-				duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.TransformConstraintTimeline.ENTRIES])
 			end
 		end
 
 		-- Path constraint timelines.
 		if map.path then
 			for constraintName,constraintMap in pairs(map.path) do
-				local index = skeletonData:findPathConstraintIndex(constraintName)
-				if index == -1 then error("Path constraint not found: " .. constraintName, 2) end
-				local data = skeletonData.pathConstraints[index]
+				local constraint, constraintIndex = -1
+				for i,other in pairs(skeletonData.transformConstraints) do
+					if other.name == constraintName then
+						constraintIndex = i
+						constraint = other
+						break
+					end
+				end
 				for timelineName, timelineMap in pairs(constraintMap) do
-					if timelineName == "position" or timelineName == "spacing" then
-						local timeline = nil
-						local timelineScale = 1
-						if timelineName == "spacing" then
-							timeline = Animation.PathConstraintSpacingTimeline.new(#timelineMap)
-							if data.spacingMode == PathConstraintData.SpacingMode.length or data.spacingMode == PathConstraintData.SpacingMode.fixed then timelineScale = scale end
-						else
-							timeline = Animation.PathConstraintPositionTimeline.new(#timelineMap)
-							if data.positionMode == PathConstraintData.PositionMode.fixed then timelineScale = scale end
-						end
-						timeline.pathConstraintIndex = index
-						local frameIndex = 0
-						for _,valueMap in ipairs(timelineMap) do
-							timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), getValue(valueMap, timelineName, 0) * timelineScale)
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
-						end
-						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.PathConstraintPositionTimeline.ENTRIES])
-					elseif timelineName == "mix" then
-						local timeline = Animation.PathConstraintMixTimeline.new(#timelineMap)
-						timeline.pathConstraintIndex = index
-						local frameIndex = 0
-						for _,valueMap in ipairs(timelineMap) do
-							timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), getValue(valueMap, "rotateMix", 1), getValue(valueMap, "translateMix", 1))
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
+					local keyMap = timelineMap[1]
+					if keyMap then
+						if timelineName == "position" then
+							local timeline = Animation.PathConstraintPositionTimeline.new(#timelineMap, #timelineMap, constraintIndex)
+							local timelineScale = 1
+							if constraint.positionMode == PositionMode.fixed then timelineScale = scale end
+							table_insert(timelines, readTimeline1(timelineMap, timeline, 0, timelineScale))
+						elseif timelineName == "spacing" then
+							local timeline = Animation.PathConstraintSpacingTimeline.new(#timelineMap, #timelineMap, constraintIndex)
+							local timelineScale = 1;
+							if data.spacingMode == SpacingMode.Length or data.spacingMode == SpacingMode.Fixed then timelineScale = scale end
+							table_insert(timelines, readTimeline1(timelineMap, timeline, 0, timelineScale))
+						elseif timelineName == "mix" then
+							local timeline = Animation.PathConstraintMixTimeline.new(#timelineMap, #timelineMap * 3, constraintIndex)
+							local time = getValue(keyMap, "time", 0)
+							local mixRotate = getValue(keyMap, "mixRotate", 1)
+							local mixX = getValue(keyMap, "mixX", 1)
+							local mixY = getValue(keyMap, "mixY", mixX)
+							local bezier = 0
+							for i,keyMap in ipairs(timelineMap) do
+								local frame = i - 1
+								timeline:setFrame(frame, time, mixRotate, mixX, mixY)
+								local nextMap = timelineMap[frame + 1]
+								if not nextMap then
+									timeline:shrink(bezier)
+									break
+								end
+								local time2 = getValue(nextMap, "time", 0)
+								local mixRotate2 = getValue(nextMap, "mixRotate", 1)
+								local mixX2 = getValue(nextMap, "mixX", 1)
+								local mixY2 = getValue(nextMap, "mixY", mixX2)
+								local curve = keyMap.curve
+								if curve then
+									bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1)
+									bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1)
+									bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1)
+								end
+								time = time2
+								mixRotate = mixRotate2
+								mixX = mixX2
+								mixY = mixY2
+								keyMap = nextMap
+							end
+							table_insert(timelines, timeline)
 						end
-						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[(timeline:getFrameCount() - 1) * Animation.PathConstraintMixTimeline.ENTRIES])
 					end
 				end
 			end
@@ -765,68 +962,72 @@ function SkeletonJson.new (attachmentLoader)
 				local skin = skeletonData:findSkin(deformName)
 				if not skin then error("Skin not found: " .. deformName, 2) end
 				for slotName,slotMap in pairs(deformMap) do
-					local slotIndex = skeletonData:findSlotIndex(slotName)
+					local slotIndex = skeletonData:findSlot(slotName).index
 					if slotIndex == -1 then error("Slot not found: " .. slotMap.name, 2) end
 					for timelineName,timelineMap in pairs(slotMap) do
-						local attachment = skin:getAttachment(slotIndex, timelineName)
-						if not attachment then error("Deform attachment not found: " .. timelineMap.name, 2) end
-						local weighted = attachment.bones ~= nil
-						local vertices = attachment.vertices;
-						local deformLength = #vertices
-						if weighted then deformLength = math.floor(#vertices / 3) * 2 end
-
-						local timeline = Animation.DeformTimeline.new(#timelineMap)
-						timeline.slotIndex = slotIndex
-						timeline.attachment = attachment
-
-						local frameIndex = 0
-						for _,valueMap in ipairs(timelineMap) do
-							local deform = nil
-							local verticesValue = getValue(valueMap, "vertices", nil)
-							if verticesValue == nil then
-								deform = vertices
-								if weighted then deform = utils.newNumberArray(deformLength) end
-							else
-								deform = utils.newNumberArray(deformLength)
-								local start = getValue(valueMap, "offset", 0) + 1
-								utils.arrayCopy(verticesValue, 1, deform, start, #verticesValue)
-								if scale ~= 1 then
-									local i = start
-									local n = i + #verticesValue
-									while i < n do
-										deform[i] = deform[i] * scale
-										i = i + 1
+						local keyMap = timelineMap[1]
+						if keyMap then
+							local attachment = skin:getAttachment(slotIndex, timelineName)
+							if not attachment then error("Deform attachment not found: " .. timelineMap.name, 2) end
+							local weighted = attachment.bones ~= nil
+							local vertices = attachment.vertices
+							local deformLength = #vertices
+							if weighted then deformLength = math.floor(deformLength / 3) * 2 end
+
+							local timeline = Animation.DeformTimeline.new(#timelineMap, #timelineMap, slotIndex, attachment)
+							local bezier = 0
+							for i,keyMap in ipairs(timelineMap) do
+								local deform = nil
+								local verticesValue = getValue(keyMap, "vertices", nil)
+								if verticesValue == nil then
+									deform = vertices
+									if weighted then deform = utils.newNumberArray(deformLength) end
+								else
+									deform = utils.newNumberArray(deformLength)
+									local start = getValue(keyMap, "offset", 0) + 1
+									utils.arrayCopy(verticesValue, 1, deform, start, #verticesValue)
+									if scale ~= 1 then
+										local i = start
+										local n = i + #verticesValue
+										while i < n do
+											deform[i] = deform[i] * scale
+											i = i + 1
+										end
 									end
-								end
-								if not weighted then
-									local i = 1
-									local n = i + deformLength
-									while i < n do
-										deform[i] = deform[i] + vertices[i]
-										i = i + 1
+									if not weighted then
+										local i = 1
+										local n = i + deformLength
+										while i < n do
+											deform[i] = deform[i] + vertices[i]
+											i = i + 1
+										end
 									end
 								end
+								local frame = i - 1
+								timeline:setFrame(frame, time, mixRotate, mixX, mixY)
+								local nextMap = timelineMap[frame + 1]
+								if not nextMap then
+									timeline:shrink(bezier)
+									break
+								end
+								local time2 = getValue(nextMap, "time", 0)
+								local curve = keyMap.curve
+								if curve then bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, 0, 1, 1) end
+								time = time2
 							end
-
-							timeline:setFrame(frameIndex, getValue(valueMap, "time", 0), deform)
-							readCurve(valueMap, timeline, frameIndex)
-							frameIndex = frameIndex + 1
+							table_insert(timelines, timeline)
 						end
-						table_insert(timelines, timeline)
-						duration = math.max(duration, timeline.frames[timeline:getFrameCount() - 1])
 					end
 				end
 			end
 		end
 
-		-- Draworder timeline.
-		local drawOrderValues = map["drawOrder"]
-		if not drawOrderValues then drawOrderValues = map["draworder"] end
-		if drawOrderValues then
-			local timeline = Animation.DrawOrderTimeline.new(#drawOrderValues)
+		-- Draw order timelines.
+		if map["drawOrder"] then
+			local timeline = Animation.DrawOrderTimeline.new(#map["drawOrder"])
 			local slotCount = #skeletonData.slots
-			local frameIndex = 0
-			for _,drawOrderMap in ipairs(drawOrderValues) do
+			local frame = 0
+			for _,drawOrderMap in ipairs(map["drawOrder"]) do
 				local drawOrder = nil
 				local offsets = drawOrderMap["offsets"]
 				if offsets then
@@ -835,7 +1036,7 @@ function SkeletonJson.new (attachmentLoader)
 					local originalIndex = 1
 					local unchangedIndex = 1
 					for _,offsetMap in ipairs(offsets) do
-						local slotIndex = skeletonData:findSlotIndex(offsetMap["slot"])
+						local slotIndex = skeletonData:findSlot(offsetMap["slot"]).index
 						if slotIndex == -1 then error("Slot not found: " .. offsetMap["slot"]) end
 						-- Collect unchanged items.
 						while originalIndex ~= slotIndex do
@@ -861,18 +1062,17 @@ function SkeletonJson.new (attachmentLoader)
 						end
 					end
 				end
-				timeline:setFrame(frameIndex, getValue(drawOrderMap, "time", 0), drawOrder)
-				frameIndex = frameIndex + 1
+				timeline:setFrame(frame, getValue(drawOrderMap, "time", 0), drawOrder)
+				frame = frame + 1
 			end
 			table_insert(timelines, timeline)
-			duration = math.max(duration, timeline.frames[timeline:getFrameCount() - 1])
 		end
 
-		-- Event timeline.
+		-- Event timelines.
 		local events = map["events"]
 		if events then
 			local timeline = Animation.EventTimeline.new(#events)
-			local frameIndex = 0
+			local frame = 0
 			for _,eventMap in ipairs(events) do
 				local eventData = skeletonData:findEvent(eventMap["name"])
 				if not eventData then error("Event not found: " .. eventMap["name"]) end
@@ -896,24 +1096,89 @@ function SkeletonJson.new (attachmentLoader)
 					event.volume = getValue(eventMap, "volume", 1)
 					event.balance = getValue(eventMap, "balance", 0)
 				end
-				timeline:setFrame(frameIndex, event)
-				frameIndex = frameIndex + 1
+				timeline:setFrame(frame, event)
+				frame = frame + 1
 			end
 			table_insert(timelines, timeline)
-			duration = math.max(duration, timeline.frames[timeline:getFrameCount() - 1])
 		end
 
+		local duration = 0
+		for _,timeline in ipairs(timelines) do
+			duration = math.max(duration, timeline:getDuration())
+		end
 		table_insert(skeletonData.animations, Animation.new(name, timelines, duration))
 	end
 
-	readCurve = function (map, timeline, frameIndex)
+	readCurve = function (map, timeline, frame)
 		local curve = map["curve"]
 		if not curve then return end
 		if curve == "stepped" then
-			timeline:setStepped(frameIndex)
+			timeline:setStepped(frame)
 		else
-			timeline:setCurve(frameIndex, getValue(map, "curve", 0), getValue(map, "c2", 0), getValue(map, "c3", 1), getValue(map, "c4", 1))
+			timeline:setCurve(frame, getValue(map, "curve", 0), getValue(map, "c2", 0), getValue(map, "c3", 1), getValue(map, "c4", 1))
+		end
+	end
+
+	readTimeline1 = function (keys, timeline, defaultValue, scale)
+		local keyMap = keys[1]
+		local time = getValue(keyMap, "time", 0)
+		local value = getValue(keyMap, "value", defaultValue) * scale
+		local bezier = 0
+		for i,keyMap in ipairs(keys) do
+			local frame = i - 1
+			timeline:setFrame(frame, time, value)
+			local nextMap = keys[frame + 1]
+			if not nextMap then break end
+			local time2 = getValue(nextMap, "time", 0)
+			local value2 = getValue(nextMap, "value", defaultValue) * scale
+			local curve = keyMap.curve
+			if curve then bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, value, value2, scale) end
+			time = time2
+			value = value2
+		end
+		timeline:shrink(bezier)
+		return timeline
+	end
+
+	readTimeline2 = function (keys, timeline, name1, name2, defaultValue, scale)
+		local keyMap = keys[1]
+		local time = getValue(keyMap, "time", 0)
+		local value1 = getValue(keyMap, name1, defaultValue) * scale
+		local value2 = getValue(keyMap, name2, defaultValue) * scale
+		local bezier = 0
+		for i,keyMap in ipairs(keys) do
+			local frame = i - 1
+			timeline:setFrame(frame, time, value1, value2)
+			local nextMap = keys[frame + 1]
+			if not nextMap then break end
+			local time2 = getValue(nextMap, "time", 0)
+			local nvalue1 = getValue(nextMap, name1, defaultValue) * scale
+			local nvalue2 = getValue(nextMap, name2, defaultValue) * scale
+			local curve = keyMap.curve
+			if curve then
+				bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, value1, nvalue1, scale)
+				bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, value2, nvalue2, scale)
+			end
+			time = time2
+			value1 = nvalue1
+			value2 = nvalue2
+		end
+		timeline:shrink(bezier)
+		return timeline
+	end
+
+	readCurve = function (curve, timeline, bezier, frame, value, time1, time2, value1, value2, scale)
+		if curve == "stepped" then
+			if value ~= 0 then timeline.setStepped(frame) end
+			return bezier
 		end
+		local i = value * 4
+		local cx1 = curve[i]
+		local cy1 = curve[i + 1] * scale
+		local cx2 = curve[i + 2]
+		local cy2 = curve[i + 3] * scale
+		timeline.setBezier(bezier, frame, value, time1, value1, cx1, cy1, cx2, cy2, time2, value2)
+		return bezier + 1
 	end
 
 	getArray = function (map, name, scale)

+ 4 - 4
spine-lua/spine-lua/TransformConstraint.lua

@@ -97,7 +97,7 @@ function TransformConstraint:applyAbsoluteWorld ()
 	local tb = target.b
 	local tc = target.c
 	local td = target.d
-	local degRadReflect = 0;
+	local degRadReflect = 0
 	if ta * td - tb * tc > 0 then degRadReflect = utils.degRad else degRadReflect = -utils.degRad end
 	local offsetRotation = self.data.offsetRotation * degRadReflect
 	local offsetShearY = self.data.offsetShearY * degRadReflect
@@ -184,7 +184,7 @@ function TransformConstraint:applyRelativeWorld ()
 	local tb = target.b
 	local tc = target.c
 	local td = target.d
-	local degRadReflect = 0;
+	local degRadReflect = 0
 	if ta * td - tb * tc > 0 then degRadReflect = utils.degRad else degRadReflect = -utils.degRad end
 	local offsetRotation = self.data.offsetRotation * degRadReflect
 	local offsetShearY = self.data.offsetShearY * degRadReflect
@@ -242,7 +242,7 @@ function TransformConstraint:applyRelativeWorld ()
 			end
 			local b = bone.b
 			local d = bone.d
-			r = math_atan2(d, b) + (r - math_pi / 2 + offsetShearY) * shearMix;
+			r = math_atan2(d, b) + (r - math_pi / 2 + offsetShearY) * shearMix
 			local s = math_sqrt(b * b + d * d)
 			bone.b = math_cos(r) * s
 			bone.d = math_sin(r) * s
@@ -297,7 +297,7 @@ function TransformConstraint:applyAbsoluteLocal ()
 			bone.shearY = bone.shearY + r * shearMix
 		end
 
-		bone:updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
+		bone:updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY)
 	end
 end
 

+ 21 - 21
spine-lua/spine-lua/Triangulator.lua

@@ -74,7 +74,7 @@ function Triangulator:triangulate (verticesArray)
 	end
 
 	self.triangles = {}
-	local triangles = self.triangles;
+	local triangles = self.triangles
 
 	while vertexCount > 3 do
 		-- Find ear tip.
@@ -122,7 +122,7 @@ function Triangulator:triangulate (verticesArray)
 			if _next == 0 then
 				repeat
 					if not isConcave[i] then
-						break;
+						break
 					end
 					i = i - 1
 				until i == 0
@@ -171,10 +171,10 @@ function Triangulator:decompose(verticesArray, triangles)
 	local vertices = verticesArray
 
 	self.convexPolygons = {}
-	local convexPolygons = self.convexPolygons;
+	local convexPolygons = self.convexPolygons
 
 	self.convexPolygonsIndices = {}
-	local convexPolygonsIndices = self.convexPolygonsIndices;
+	local convexPolygonsIndices = self.convexPolygonsIndices
 
 	local polygonIndices = {}
 	local polygon = {}
@@ -197,12 +197,12 @@ function Triangulator:decompose(verticesArray, triangles)
 		local y3 = vertices[t3 + 1]
 
 		-- If the base of the last triangle is the same as this triangle, check if they form a convex polygon (triangle fan).
-		local merged = false;
+		local merged = false
 		if fanBaseIndex == t1 then
-			local o = #polygon - 4 + 1;
-			local p = polygon;
-			local winding1 = self:winding(p[o], p[o + 1], p[o + 2], p[o + 3], x3, y3);
-			local winding2 = self:winding(x3, y3, p[1], p[2], p[3], p[4]);
+			local o = #polygon - 4 + 1
+			local p = polygon
+			local winding1 = self:winding(p[o], p[o + 1], p[o + 2], p[o + 3], x3, y3)
+			local winding2 = self:winding(x3, y3, p[1], p[2], p[3], p[4])
 			if winding1 == lastWinding and winding2 == lastWinding then
 				table_insert(polygon, x3)
 				table_insert(polygon, y3)
@@ -226,8 +226,8 @@ function Triangulator:decompose(verticesArray, triangles)
 			table_insert(polygon, y3)
 			polygonIndices = {}
 			table_insert(polygonIndices, t1)
-			table_insert(polygonIndices, t2);
-			table_insert(polygonIndices, t3);
+			table_insert(polygonIndices, t2)
+			table_insert(polygonIndices, t3)
 			lastWinding = self:winding(x1, y1, x2, y2, x3, y3)
 			fanBaseIndex = t1
 		end
@@ -266,11 +266,11 @@ function Triangulator:decompose(verticesArray, triangles)
 				if ii ~= i then
 					local otherIndices = convexPolygonsIndices[ii]
 					if (#otherIndices == 3) then
-						local otherFirstIndex = otherIndices[1];
-						local otherSecondIndex = otherIndices[2];
-						local otherLastIndex = otherIndices[3];
+						local otherFirstIndex = otherIndices[1]
+						local otherSecondIndex = otherIndices[2]
+						local otherLastIndex = otherIndices[3]
 
-						local otherPoly = convexPolygons[ii];
+						local otherPoly = convexPolygons[ii]
 						local x3 = otherPoly[#otherPoly - 2 + 1]
 						local y3 = otherPoly[#otherPoly - 1 + 1]
 
@@ -308,14 +308,14 @@ function Triangulator:decompose(verticesArray, triangles)
 		i = i - 1
 	end
 
-	return convexPolygons;
+	return convexPolygons
 end
 
 function Triangulator:isConcave(index, vertexCount, vertices, indices)
-	local previous = indices[(vertexCount + index - 1) % vertexCount] * 2 + 1;
-	local current = indices[index] * 2 + 1;
-	local _next = indices[(index + 1) % vertexCount] * 2 + 1;
-	return not self:positiveArea(vertices[previous], vertices[previous + 1], vertices[current], vertices[current + 1], vertices[_next],vertices[_next + 1]);
+	local previous = indices[(vertexCount + index - 1) % vertexCount] * 2 + 1
+	local current = indices[index] * 2 + 1
+	local _next = indices[(index + 1) % vertexCount] * 2 + 1
+	return not self:positiveArea(vertices[previous], vertices[previous + 1], vertices[current], vertices[current + 1], vertices[_next],vertices[_next + 1])
 end
 
 function Triangulator:positiveArea(p1x, p1y, p2x, p2y, p3x, p3y)
@@ -328,7 +328,7 @@ function Triangulator:winding(p1x, p1y, p2x, p2y, p3x, p3y)
 	if p3x * py - p3y * px + px * p1y - p1x * py >= 0 then
 		return 1
 	else
-		return -1;
+		return -1
 	end
 end
 

+ 12 - 12
spine-lua/spine-lua/attachments/MeshAttachment.lua

@@ -84,8 +84,8 @@ function MeshAttachment:updateUVs ()
 			local i = 0
 			local n = #uvs
 			while i < n do
-				uvs[i + 1] = u + regionUVs[i + 2] * width;
-				uvs[i + 2] = v + (1 - regionUVs[i + 1]) * height;
+				uvs[i + 1] = u + regionUVs[i + 2] * width
+				uvs[i + 2] = v + (1 - regionUVs[i + 1]) * height
 				i = i + 2
 			end
 		elseif region.degrees == 180 then
@@ -96,8 +96,8 @@ function MeshAttachment:updateUVs ()
 			local i = 0
 			local n = #uvs
 			while i < n do
-				uvs[i + 1] = u + (1 - regionUVs[i + 1]) * width;
-				uvs[i + 2] = v + (1 - regionUVs[i + 2]) * height;
+				uvs[i + 1] = u + (1 - regionUVs[i + 1]) * width
+				uvs[i + 2] = v + (1 - regionUVs[i + 2]) * height
 				i = i + 2
 			end
 		elseif region.degrees == 270 then
@@ -108,20 +108,20 @@ function MeshAttachment:updateUVs ()
 			local i = 0
 			local n = #uvs
 			while i < n do
-				uvs[i + 1] = u + (1 - regionUVs[i + 2]) * width;
-				uvs[i + 2] = v + regionUVs[i + 1] * height;
+				uvs[i + 1] = u + (1 - regionUVs[i + 2]) * width
+				uvs[i + 2] = v + regionUVs[i + 1] * height
 				i = i + 2
 			end
 		else
-			u = region.u - region.offsetX / textureWidth;
-			v = region.v - (region.originalHeight - region.offsetY - region.height) / textureHeight;
-			width = region.originalWidth / textureWidth;
-			height = region.originalHeight / textureHeight;
+			u = region.u - region.offsetX / textureWidth
+			v = region.v - (region.originalHeight - region.offsetY - region.height) / textureHeight
+			width = region.originalWidth / textureWidth
+			height = region.originalHeight / textureHeight
 			local i = 0
 			local n = #uvs
 			while i < n do
-				uvs[i + 1] = u + regionUVs[i + 1] * width;
-				uvs[i + 2] = v + regionUVs[i + 2] * height;
+				uvs[i + 1] = u + regionUVs[i + 1] * width
+				uvs[i + 2] = v + regionUVs[i + 2] * height
 				i = i + 2
 			end
 		end

+ 1 - 0
spine-lua/spine-lua/attachments/RegionAttachment.lua

@@ -155,6 +155,7 @@ function RegionAttachment.new (name)
 end
 
 function RegionAttachment:updateOffset ()
+	if not self.region then return end
 	local regionScaleX = self.width / self.region.originalWidth * self.scaleX
 	local regionScaleY = self.height / self.region.originalHeight * self.scaleY
 	local localX = -self.width / 2 * self.scaleX + self.region.offsetX * regionScaleX

+ 2 - 2
spine-lua/spine-lua/attachments/VertexAttachment.lua

@@ -36,8 +36,8 @@ local utils = require "spine-lua.utils"
 local AttachmentType = require "spine-lua.attachments.AttachmentType"
 local Attachment = require "spine-lua.attachments.Attachment"
 
-local nextID = 0;
-local SHL_11 = 2048;
+local nextID = 0
+local SHL_11 = 2048
 
 local VertexAttachment = {}
 VertexAttachment.__index = VertexAttachment