------------------------------------------------------------------------------- -- Copyright (c) 2013, Esoteric Software -- All rights reserved. -- -- Redistribution and use in source and binary forms, with or without -- modification, are permitted provided that the following conditions are met: -- -- 1. Redistributions of source code must retain the above copyright notice, this -- list of conditions and the following disclaimer. -- 2. Redistributions in binary form must reproduce the above copyright notice, -- this list of conditions and the following disclaimer in the documentation -- and/or other materials provided with the distribution. -- -- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "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 THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR -- ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -- (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -- LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -- ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -- (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -- SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ------------------------------------------------------------------------------ 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, duration = duration } function self:apply (skeleton, time, loop) if not skeleton then error("skeleton cannot be nil.", 2) end if loop and duration then time = time % duration end for i,timeline in ipairs(self.timelines) do timeline:apply(skeleton, time, 1) end end function self:mix (skeleton, time, loop, alpha) if not skeleton then error("skeleton cannot be nil.", 2) end if loop and duration then time = time % duration end for i,timeline in ipairs(self.timelines) do timeline:apply(skeleton, time, alpha) end end return self end local function binarySearch (values, target, step) local low = 0 local high = math.floor((#values + 1) / 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 local function linearSearch (values, target, step) for i = 0, #values, step do if (values[i] > target) then return i end end return -1 end Animation.CurveTimeline = {} function Animation.CurveTimeline.new () local LINEAR = 0 local STEPPED = -1 local BEZIER_SEGMENTS = 10 local self = { curves = {} } function self:setLinear (keyframeIndex) self.curves[keyframeIndex * 6] = LINEAR end function self:setStepped (keyframeIndex) self.curves[keyframeIndex * 6] = STEPPED end function self:setCurve (keyframeIndex, cx1, cy1, cx2, cy2) local subdiv_step = 1 / BEZIER_SEGMENTS local subdiv_step2 = subdiv_step * subdiv_step local subdiv_step3 = subdiv_step2 * subdiv_step local pre1 = 3 * subdiv_step local pre2 = 3 * subdiv_step2 local pre4 = 6 * subdiv_step2 local pre5 = 6 * subdiv_step3 local tmp1x = -cx1 * 2 + cx2 local tmp1y = -cy1 * 2 + cy2 local tmp2x = (cx1 - cx2) * 3 + 1 local tmp2y = (cy1 - cy2) * 3 + 1 local i = keyframeIndex * 6 local curves = self.curves curves[i] = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv_step3 curves[i + 1] = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv_step3 curves[i + 2] = tmp1x * pre4 + tmp2x * pre5 curves[i + 3] = tmp1y * pre4 + tmp2y * pre5 curves[i + 4] = tmp2x * pre5 curves[i + 5] = tmp2y * pre5 end function self:getCurvePercent (keyframeIndex, percent) local curveIndex = keyframeIndex * 6 local curves = self.curves local dfx = curves[curveIndex] if not dfx then return percent end -- linear if dfx == STEPPED then return 0 end local dfy = curves[curveIndex + 1] local ddfx = curves[curveIndex + 2] local ddfy = curves[curveIndex + 3] local dddfx = curves[curveIndex + 4] local dddfy = curves[curveIndex + 5] local x = dfx local y = dfy local i = BEZIER_SEGMENTS - 2 while true do if x >= percent then local lastX = x - dfx local lastY = y - dfy return lastY + (y - lastY) * (percent - lastX) / (x - lastX) end if i == 0 then break end i = i - 1 dfx = dfx + ddfx dfy = dfy + ddfy ddfx = ddfx + dddfx ddfy = ddfy + dddfy x = x + dfx y = y + dfy end return y + (1 - y) * (percent - x) / (1 - x) -- Last point is 1,1. end return self end Animation.RotateTimeline = {} function Animation.RotateTimeline.new () local LAST_FRAME_TIME = -2 local FRAME_VALUE = 1 local self = Animation.CurveTimeline.new() self.frames = {} self.boneIndex = -1 function self:getDuration () return self.frames[#self.frames - 1] end function self:getKeyframeCount () return (#self.frames + 1) / 2 end function self:setKeyframe (keyframeIndex, time, value) keyframeIndex = keyframeIndex * 2 self.frames[keyframeIndex] = time self.frames[keyframeIndex + 1] = value end function self:apply (skeleton, time, alpha) local frames = self.frames if time < frames[0] then return end -- Time is before first frame. local bone = skeleton.bones[self.boneIndex] if time >= frames[#frames - 1] then -- Time is after last frame. local amount = bone.data.rotation + frames[#frames] - bone.rotation while amount > 180 do amount = amount - 360 end while amount < -180 do amount = amount + 360 end bone.rotation = bone.rotation + amount * alpha return end -- Interpolate between the last frame and the current frame. local frameIndex = binarySearch(frames, time, 2) local lastFrameValue = frames[frameIndex - 1] local frameTime = frames[frameIndex] local percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime) if percent < 0 then percent = 0 elseif percent > 1 then percent = 1 end percent = self:getCurvePercent(frameIndex / 2 - 1, percent) local amount = frames[frameIndex + FRAME_VALUE] - lastFrameValue while amount > 180 do amount = amount - 360 end while amount < -180 do amount = amount + 360 end amount = bone.data.rotation + (lastFrameValue + amount * percent) - bone.rotation while amount > 180 do amount = amount - 360 end while amount < -180 do amount = amount + 360 end bone.rotation = bone.rotation + amount * alpha end return self end Animation.TranslateTimeline = {} function Animation.TranslateTimeline.new () local LAST_FRAME_TIME = -3 local FRAME_X = 1 local FRAME_Y = 2 local self = Animation.CurveTimeline.new() self.frames = {} self.boneIndex = -1 function self:getDuration () return self.frames[#self.frames - 2] end function self:getKeyframeCount () return (#self.frames + 1) / 3 end function self:setKeyframe (keyframeIndex, time, x, y) keyframeIndex = keyframeIndex * 3 self.frames[keyframeIndex] = time self.frames[keyframeIndex + 1] = x self.frames[keyframeIndex + 2] = y end function self:apply (skeleton, time, alpha) local frames = self.frames if time < frames[0] then return end -- Time is before first frame. local bone = skeleton.bones[self.boneIndex] if time >= frames[#frames - 2] then -- Time is after last frame. bone.x = bone.x + (bone.data.x + frames[#frames - 1] - bone.x) * alpha bone.y = bone.y + (bone.data.y + frames[#frames] - bone.y) * alpha return end -- Interpolate between the last frame and the current frame. local frameIndex = binarySearch(frames, time, 3) local lastFrameX = frames[frameIndex - 2] local lastFrameY = frames[frameIndex - 1] local frameTime = frames[frameIndex] local percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime) if percent < 0 then percent = 0 elseif percent > 1 then percent = 1 end percent = self:getCurvePercent(frameIndex / 3 - 1, percent) bone.x = bone.x + (bone.data.x + lastFrameX + (frames[frameIndex + FRAME_X] - lastFrameX) * percent - bone.x) * alpha bone.y = bone.y + (bone.data.y + lastFrameY + (frames[frameIndex + FRAME_Y] - lastFrameY) * percent - bone.y) * alpha end return self end Animation.ScaleTimeline = {} function Animation.ScaleTimeline.new () local LAST_FRAME_TIME = -3 local FRAME_X = 1 local FRAME_Y = 2 local self = Animation.TranslateTimeline.new() function self:apply (skeleton, time, alpha) local frames = self.frames if time < frames[0] then return end -- Time is before first frame. local bone = skeleton.bones[self.boneIndex] if time >= frames[#frames - 2] then -- Time is after last frame. bone.scaleX = bone.scaleX + (bone.data.scaleX - 1 + frames[#frames - 1] - bone.scaleX) * alpha bone.scaleY = bone.scaleY + (bone.data.scaleY - 1 + frames[#frames] - bone.scaleY) * alpha return end -- Interpolate between the last frame and the current frame. local frameIndex = binarySearch(frames, time, 3) local lastFrameX = frames[frameIndex - 2] local lastFrameY = frames[frameIndex - 1] local frameTime = frames[frameIndex] local percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime) if percent < 0 then percent = 0 elseif percent > 1 then percent = 1 end percent = self:getCurvePercent(frameIndex / 3 - 1, percent) bone.scaleX = bone.scaleX + (bone.data.scaleX - 1 + lastFrameX + (frames[frameIndex + FRAME_X] - lastFrameX) * percent - bone.scaleX) * alpha bone.scaleY = bone.scaleY + (bone.data.scaleY - 1 + lastFrameY + (frames[frameIndex + FRAME_Y] - lastFrameY) * percent - bone.scaleY) * alpha end return self end Animation.ColorTimeline = {} function Animation.ColorTimeline.new () local LAST_FRAME_TIME = -5 local FRAME_R = 1 local FRAME_G = 2 local FRAME_B = 3 local FRAME_A = 4 local self = Animation.CurveTimeline.new() self.frames = {} self.slotIndex = -1 function self:getDuration () return self.frames[#self.frames - 4] end function self:getKeyframeCount () return (#self.frames + 1) / 5 end function self:setKeyframe (keyframeIndex, time, r, g, b, a) keyframeIndex = keyframeIndex * 5 self.frames[keyframeIndex] = time self.frames[keyframeIndex + 1] = r self.frames[keyframeIndex + 2] = g self.frames[keyframeIndex + 3] = b self.frames[keyframeIndex + 4] = a end function self:apply (skeleton, time, alpha) local frames = self.frames if time < frames[0] then return end -- Time is before first frame. local slot = skeleton.slots[self.slotIndex] if time >= frames[#frames - 4] then -- Time is after last frame. local r = frames[#frames - 3] local g = frames[#frames - 2] local b = frames[#frames - 1] local a = frames[#frames] slot:setColor(r, g, b, a) return end -- Interpolate between the last frame and the current frame. local frameIndex = binarySearch(frames, time, 5) local lastFrameR = frames[frameIndex - 4] local lastFrameG = frames[frameIndex - 3] local lastFrameB = frames[frameIndex - 2] local lastFrameA = frames[frameIndex - 1] local frameTime = frames[frameIndex] local percent = 1 - (time - frameTime) / (frames[frameIndex + LAST_FRAME_TIME] - frameTime) if percent < 0 then percent = 0 elseif percent > 255 then percent = 255 end percent = self:getCurvePercent(frameIndex / 5 - 1, percent) local r = lastFrameR + (frames[frameIndex + FRAME_R] - lastFrameR) * percent local g = lastFrameG + (frames[frameIndex + FRAME_G] - lastFrameG) * percent local b = lastFrameB + (frames[frameIndex + FRAME_B] - lastFrameB) * percent local a = lastFrameA + (frames[frameIndex + FRAME_A] - lastFrameA) * percent if alpha < 1 then slot:setColor(slot.r + (r - slot.r) * alpha, slot.g + (g - slot.g) * alpha, slot.b + (b - slot.b) * alpha, slot.a + (a - slot.a) * alpha) else slot:setColor(r, g, b, a) end end return self end Animation.AttachmentTimeline = {} function Animation.AttachmentTimeline.new () local self = Animation.CurveTimeline.new() self.frames = {} self.attachmentNames = {} self.slotName = nil function self:getDuration () return self.frames[#self.frames] end function self:getKeyframeCount () return #self.frames + 1 end function self:setKeyframe (keyframeIndex, time, attachmentName) self.frames[keyframeIndex] = time self.attachmentNames[keyframeIndex] = attachmentName end function self:apply (skeleton, time, alpha) local frames = self.frames if time < frames[0] then return end -- Time is before first frame. local frameIndex if time >= frames[#frames] then -- Time is after last frame. frameIndex = #frames else frameIndex = binarySearch(frames, time, 1) - 1 end local attachmentName = self.attachmentNames[frameIndex] local slot = skeleton.slotsByName[self.slotName] if attachmentName then if not slot.attachment then slot:setAttachment(skeleton:getAttachment(self.slotName, attachmentName)) elseif slot.attachment.name ~= attachmentName then slot:setAttachment(skeleton:getAttachment(self.slotName, attachmentName)) end else slot:setAttachment(nil) end end return self end return Animation