spine-runtimes/spine-lua/Animation.lua
NathanSweet e114f04bba Don't mix from uninitialized slot vertices.
Related to 27bab7bb951650c50c98b3b9b7bcc1fcb90c1570.
2016-03-29 03:14:03 +02:00

779 lines
22 KiB
Lua

-------------------------------------------------------------------------------
-- Spine Runtimes Software License
-- Version 2.3
--
-- Copyright (c) 2013-2015, Esoteric Software
-- All rights reserved.
--
-- You are granted a perpetual, non-exclusive, non-sublicensable and
-- non-transferable license to use, install, execute and perform the Spine
-- Runtimes Software (the "Software") and derivative works solely for personal
-- or internal use. Without the written permission of Esoteric Software (see
-- Section 2 of the Spine Software License Agreement), you may not (a) modify,
-- translate, adapt or otherwise create derivative works, improvements of the
-- Software or develop new applications using the Software or (b) remove,
-- delete, alter or obscure any trademarks or any copyright, trademark, patent
-- or other intellectual property or proprietary rights notices on or in the
-- Software, including any copy thereof. Redistributions in binary or source
-- form must include this license and terms.
--
-- THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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, lastTime, time, loop, events)
if not skeleton then error("skeleton cannot be nil.", 2) end
if loop and duration > 0 then
time = time % self.duration
lastTime = lastTime % self.duration
end
for i,timeline in ipairs(self.timelines) do
timeline:apply(skeleton, lastTime, time, events, 1)
end
end
function self:mix (skeleton, lastTime, time, loop, events, alpha)
if not skeleton then error("skeleton cannot be nil.", 2) end
if loop and duration > 0 then
time = time % self.duration
lastTime = lastTime % self.duration
end
for i,timeline in ipairs(self.timelines) do
timeline:apply(skeleton, lastTime, time, events, 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 binarySearch1 (values, target)
local low = 0
local high = math.floor(#values - 1)
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)
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 = 2;
local BEZIER_SEGMENTS = 10
local BEZIER_SIZE = BEZIER_SEGMENTS * 2 - 1
local self = {
curves = {} -- type, x, y, ...
}
function self:setLinear (frameIndex)
self.curves[frameIndex * BEZIER_SIZE] = LINEAR
end
function self:setStepped (frameIndex)
self.curves[frameIndex * BEZIER_SIZE] = STEPPED
end
function self:setCurve (frameIndex, cx1, cy1, cx2, cy2)
local subdiv1 = 1 / BEZIER_SEGMENTS
local subdiv2 = subdiv1 * subdiv1
local subdiv3 = subdiv2 * subdiv1;
local pre1 = 3 * subdiv1
local pre2 = 3 * subdiv2
local pre4 = 6 * subdiv2
local pre5 = 6 * subdiv3
local tmp1x = -cx1 * 2 + cx2
local tmp1y = -cy1 * 2 + cy2
local tmp2x = (cx1 - cx2) * 3 + 1
local tmp2y = (cy1 - cy2) * 3 + 1
local dfx = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv3
local dfy = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv3
local ddfx = tmp1x * pre4 + tmp2x * pre5
local ddfy = tmp1y * pre4 + tmp2y * pre5;
local dddfx = tmp2x * pre5
local dddfy = tmp2y * pre5
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)
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 = {}
function Animation.RotateTimeline.new ()
local PREV_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:getFrameCount ()
return (#self.frames + 1) / 2
end
function self:setFrame (frameIndex, time, value)
frameIndex = frameIndex * 2
self.frames[frameIndex] = time
self.frames[frameIndex + 1] = value
end
function self:apply (skeleton, lastTime, time, firedEvents, 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 prevFrameValue = frames[frameIndex - 1]
local frameTime = frames[frameIndex]
local percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_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] - prevFrameValue
while amount > 180 do
amount = amount - 360
end
while amount < -180 do
amount = amount + 360
end
amount = bone.data.rotation + (prevFrameValue + 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 PREV_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:getFrameCount ()
return (#self.frames + 1) / 3
end
function self:setFrame (frameIndex, time, x, y)
frameIndex = frameIndex * 3
self.frames[frameIndex] = time
self.frames[frameIndex + 1] = x
self.frames[frameIndex + 2] = y
end
function self:apply (skeleton, lastTime, time, firedEvents, 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 prevFrameX = frames[frameIndex - 2]
local prevFrameY = frames[frameIndex - 1]
local frameTime = frames[frameIndex]
local percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_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 + prevFrameX + (frames[frameIndex + FRAME_X] - prevFrameX) * percent - bone.x) * alpha
bone.y = bone.y + (bone.data.y + prevFrameY + (frames[frameIndex + FRAME_Y] - prevFrameY) * percent - bone.y) * alpha
end
return self
end
Animation.ScaleTimeline = {}
function Animation.ScaleTimeline.new ()
local PREV_FRAME_TIME = -3
local FRAME_X = 1
local FRAME_Y = 2
local self = Animation.TranslateTimeline.new()
function self:apply (skeleton, lastTime, time, firedEvents, 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 * frames[#frames - 1] - bone.scaleX) * alpha
bone.scaleY = bone.scaleY + (bone.data.scaleY * frames[#frames] - bone.scaleY) * alpha
return
end
-- Interpolate between the last frame and the current frame.
local frameIndex = binarySearch(frames, time, 3)
local prevFrameX = frames[frameIndex - 2]
local prevFrameY = frames[frameIndex - 1]
local frameTime = frames[frameIndex]
local percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_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 * (prevFrameX + (frames[frameIndex + FRAME_X] - prevFrameX) * percent) - bone.scaleX) * alpha
bone.scaleY = bone.scaleY + (bone.data.scaleY * (prevFrameY + (frames[frameIndex + FRAME_Y] - prevFrameY) * percent) - bone.scaleY) * alpha
end
return self
end
Animation.ColorTimeline = {}
function Animation.ColorTimeline.new ()
local PREV_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:getFrameCount ()
return (#self.frames + 1) / 5
end
function self:setFrame (frameIndex, time, r, g, b, a)
frameIndex = frameIndex * 5
self.frames[frameIndex] = time
self.frames[frameIndex + 1] = r
self.frames[frameIndex + 2] = g
self.frames[frameIndex + 3] = b
self.frames[frameIndex + 4] = a
end
function self:apply (skeleton, lastTime, time, firedEvents, alpha)
local frames = self.frames
if time < frames[0] then return end -- Time is before first frame.
local r, g, b, a
if time >= frames[#frames - 4] then -- Time is after last frame.
r = frames[#frames - 3]
g = frames[#frames - 2]
b = frames[#frames - 1]
a = frames[#frames]
else
-- Interpolate between the last frame and the current frame.
local frameIndex = binarySearch(frames, time, 5)
local prevFrameR = frames[frameIndex - 4]
local prevFrameG = frames[frameIndex - 3]
local prevFrameB = frames[frameIndex - 2]
local prevFrameA = frames[frameIndex - 1]
local frameTime = frames[frameIndex]
local percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime)
if percent < 0 then percent = 0 elseif percent > 255 then percent = 255 end
percent = self:getCurvePercent(frameIndex / 5 - 1, percent)
r = prevFrameR + (frames[frameIndex + FRAME_R] - prevFrameR) * percent
g = prevFrameG + (frames[frameIndex + FRAME_G] - prevFrameG) * percent
b = prevFrameB + (frames[frameIndex + FRAME_B] - prevFrameB) * percent
a = prevFrameA + (frames[frameIndex + FRAME_A] - prevFrameA) * percent
end
local slot = skeleton.slots[self.slotIndex]
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 = {
frames = {}, -- time, ...
attachmentNames = {},
slotName = nil
}
function self:getDuration ()
return self.frames[#self.frames]
end
function self:getFrameCount ()
return #self.frames + 1
end
function self:setFrame (frameIndex, time, attachmentName)
self.frames[frameIndex] = time
self.attachmentNames[frameIndex] = attachmentName
end
function self:apply (skeleton, lastTime, time, firedEvents, alpha)
local frames = self.frames
if time < frames[0] then
if lastTime > time then self:apply(skeleton, lastTime, 999999, nil, 0) end
return
elseif lastTime > time then
lastTime = -1
end
local frameIndex
if time >= frames[#frames] then
frameIndex = #frames
else
frameIndex = binarySearch1(frames, time) - 1
end
if frames[frameIndex] < lastTime then return 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
Animation.EventTimeline = {}
function Animation.EventTimeline.new ()
local self = {
frames = {},
events = {}
}
function self:getDuration ()
return self.frames[#self.frames]
end
function self:getFrameCount ()
return #self.frames + 1
end
function self:setFrame (frameIndex, time, event)
self.frames[frameIndex] = time
self.events[frameIndex] = event
end
-- Fires events for frames > lastTime and <= time.
function self:apply (skeleton, lastTime, time, firedEvents, alpha)
if not firedEvents then return end
local frames = self.frames
local frameCount = #frames + 1
if lastTime > time then -- Fire events after last time for looped animations.
self:apply(skeleton, lastTime, 999999, firedEvents, alpha)
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 frameIndex
if lastTime < frames[0] then
frameIndex = 0
else
frameIndex = binarySearch1(frames, lastTime)
local frame = frames[frameIndex]
while frameIndex > 0 do -- Fire multiple events with the same frame.
if frames[frameIndex - 1] ~= frame then break end
frameIndex = frameIndex - 1
end
end
local events = self.events
while frameIndex < frameCount and time >= frames[frameIndex] do
table.insert(firedEvents, events[frameIndex])
frameIndex = frameIndex + 1
end
end
return self
end
Animation.DrawOrderTimeline = {}
function Animation.DrawOrderTimeline.new ()
local self = {
frames = {},
drawOrders = {}
}
function self:getDuration ()
return self.frames[#self.frames]
end
function self:getFrameCount ()
return #self.frames + 1
end
function self:setFrame (frameIndex, time, drawOrder)
self.frames[frameIndex] = time
self.drawOrders[frameIndex] = drawOrder
end
function self:apply (skeleton, lastTime, time, firedEvents, 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 = binarySearch1(frames, time) - 1
end
local drawOrder = skeleton.drawOrder
local slots = skeleton.slots
local drawOrderToSetupIndex = self.drawOrders[frameIndex]
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.FfdTimeline = {}
function Animation.FfdTimeline.new ()
local self = Animation.CurveTimeline.new()
self.frames = {} -- time, ...
self.frameVertices = {}
self.slotIndex = -1
function self:getDuration ()
return self.frames[#self.frames]
end
function self:getFrameCount ()
return #self.frames + 1
end
function self:setFrame (frameIndex, time, vertices)
self.frames[frameIndex] = time
self.frameVertices[frameIndex] = vertices
end
function self:apply (skeleton, lastTime, time, firedEvents, alpha)
local slot = skeleton.slots[self.slotIndex]
if slot.attachment ~= self.attachment then return end
local frames = self.frames
if time < frames[0] then return end -- Time is before first frame.
local frameVertices = self.frameVertices
local vertexCount = #frameVertices[0]
local vertices = slot.attachmentVertices
if not vertices or #vertices ~= vertexCount then
if #vertices < vertexCount then
vertices = {}
slot.attachmentVertices = vertices
end
alpha = 1 -- Don't mix from uninitialized slot vertices.
end
slot.attachmentVerticesCount = vertexCount
if time >= frames[#frames - 1] then -- Time is after last frame.
local lastVertices = frameVertices[#frames - 1]
if alpha < 1 then
for i = 1, vertexCount do
local vertex = vertices[i]
vertices[i] = vertex + (lastVertices[i] - vertex) * alpha
end
else
for i = 1, vertexCount do
vertices[i] = lastVertices[i]
end
end
return
end
-- Interpolate between the previous frame and the current frame.
local frameIndex = binarySearch1(frames, time)
local frameTime = frames[frameIndex]
local percent = 1 - (time - frameTime) / (frames[frameIndex - 1] - frameTime)
if percent < 0 then percent = 0 elseif percent > 1 then percent = 1 end
percent = self:getCurvePercent(frameIndex - 1, percent)
local prevVertices = frameVertices[frameIndex - 1]
local nextVertices = frameVertices[frameIndex]
if alpha < 1 then
for i = 1, vertexCount do
local prev = prevVertices[i]
local vertex = vertices[i]
vertices[i] = vertex + (prev + (nextVertices[i] - prev) * percent - vertex) * alpha
end
else
for i = 1, vertexCount do
local prev = prevVertices[i]
vertices[i] = prev + (nextVertices[i] - prev) * percent
end
end
end
return self
end
Animation.IkConstraintTimeline = {}
function Animation.IkConstraintTimeline.new ()
local PREV_FRAME_TIME = -3
local PREV_FRAME_MIX = -2
local PREV_FRAME_BEND_DIRECTION = -1
local FRAME_MIX = 1
local self = Animation.CurveTimeline.new()
self.frames = {} -- time, mix, bendDirection, ...
self.ikConstraintIndex = -1
function self:getDuration ()
return self.frames[#self.frames - 2]
end
function self:getFrameCount ()
return (#self.frames + 1) / 3
end
function self:setFrame (frameIndex, time, mix, bendDirection)
frameIndex = frameIndex * 3
self.frames[frameIndex] = time
self.frames[frameIndex + 1] = mix
self.frames[frameIndex + 2] = bendDirection
end
function self:apply (skeleton, lastTime, time, firedEvents, alpha)
local frames = self.frames
if time < frames[0] then return end -- Time is before first frame.
local ikConstraint = skeleton.ikConstraints[self.ikConstraintIndex]
if time >= frames[#frames - 2] then -- Time is after last frame.
ikConstraint.mix = ikConstraint.mix + (frames[#frames - 1] - ikConstraint.mix) * alpha
ikConstraint.bendDirection = frames[#frames]
return
end
-- Interpolate between the previous frame and the current frame.
local frameIndex = binarySearch(frames, time, 3);
local prevFrameMix = frames[frameIndex + PREV_FRAME_MIX]
local frameTime = frames[frameIndex]
local percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime)
if percent < 0 then percent = 0 elseif percent > 1 then percent = 1 end
percent = self:getCurvePercent(frameIndex / 3 - 1, percent)
local mix = prevFrameMix + (frames[frameIndex + FRAME_MIX] - prevFrameMix) * percent
ikConstraint.mix = ikConstraint.mix + (mix - ikConstraint.mix) * alpha
ikConstraint.bendDirection = frames[frameIndex + PREV_FRAME_BEND_DIRECTION]
end
return self
end
Animation.FlipXTimeline = {}
function Animation.FlipXTimeline.new ()
local self = {
frames = {}, -- time, flip, ...
boneIndex = -1
}
function self:getDuration ()
return self.frames[#self.frames - 1]
end
function self:getFrameCount ()
return (#self.frames + 1) / 2
end
function self:setFrame (frameIndex, time, flip)
frameIndex = frameIndex * 2
self.frames[frameIndex] = time
self.frames[frameIndex + 1] = flip
end
function self:apply (skeleton, lastTime, time, firedEvents, alpha)
local frames = self.frames
if time < frames[0] then
if lastTime > time then self:apply(skeleton, lastTime, 999999, nil, 0) end
return
elseif lastTime > time then
lastTime = -1
end
local frameIndex
if time >= frames[#frames - 1] then
frameIndex = #frames - 1
else
frameIndex = binarySearch(frames, time, 2) - 2
end
if frames[frameIndex] < lastTime then return end
self:setFlip(skeleton.bones[self.boneIndex], frames[frameIndex + 1])
end
function self:setFlip (bone, flip)
bone.flipX = flip
end
return self
end
Animation.FlipYTimeline = {}
function Animation.FlipYTimeline.new ()
local self = Animation.FlipXTimeline.new()
function self:setFlip (bone, flip)
bone.flipY = flip
end
return self
end
return Animation