spine-runtimes/spine-lua/AnimationState.lua
NathanSweet df70264a5b Go back to clearing the entry after the "end" listeners.
Changing the AnimationState state from an "end" listener is problematic even when the entry is cleared before the listener, so better to make the track that is ending available to the listener.
2014-03-17 16:53:33 +01:00

249 lines
7.9 KiB
Lua

-------------------------------------------------------------------------------
-- Spine Runtimes Software License
-- Version 2
--
-- Copyright (c) 2013, Esoteric Software
-- All rights reserved.
--
-- You are granted a perpetual, non-exclusive, non-sublicensable and
-- non-transferable license to install, execute and perform the Spine Runtimes
-- Software (the "Software") solely for internal use. Without the written
-- permission of Esoteric Software, 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 SOFTARE 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 AnimationState = {}
function AnimationState.new (data)
if not data then error("data cannot be nil", 2) end
local self = {
data = data,
tracks = {},
trackCount = 0,
events = {},
onStart = nil, onEnd = nil, onComplete = nil, onEvent = nil,
timeScale = 1
}
local function setCurrent (index, entry)
local current = self.tracks[index]
if current then
local previous = current.previous
current.previous = nil
if current.onEnd then current.onEnd(index) end
if self.onEnd then self.onEnd(index) end
entry.mixDuration = self.data:getMix(current.animation.name, entry.animation.name)
if entry.mixDuration > 0 then
entry.mixTime = 0
-- If a mix is in progress, mix from the closest animation.
if previous and current.mixTime / current.mixDuration < 0.5 then
entry.previous = previous
else
entry.previous = current
end
end
end
self.tracks[index] = entry
self.trackCount = math.max(self.trackCount, index)
if entry.onStart then entry.onStart(index) end
if self.onStart then self.onStart(index) end
end
function self:update (delta)
delta = delta * self.timeScale
for i = 0, self.trackCount do
local current = self.tracks[i]
if current then
local trackDelta = delta * current.timeScale
current.time = current.time + trackDelta
if current.previous then
current.previous.time = current.previous.time + trackDelta
current.mixTime = current.mixTime + trackDelta
end
local next = current.next
if next then
if current.lastTime >= next.delay then setCurrent(i, next) end
else
-- End non-looping animation when it reaches its end time and there is no next entry.
if not current.loop and current.lastTime >= current.endTime then self:clearTrack(i) end
end
end
end
end
function self:apply(skeleton)
for i = 0, self.trackCount do
local current = self.tracks[i]
if current then
local time = current.time
local lastTime = current.lastTime
local endTime = current.endTime
local loop = current.loop
if not loop and time > endTime then time = endTime end
local previous = current.previous
if not previous then
if current.mix == 1 then
current.animation:apply(skeleton, current.lastTime, time, loop, self.events)
else
current.animation:mix(skeleton, current.lastTime, time, loop, self.events, current.mix)
end
else
local previousTime = previous.time
if not previous.loop and previousTime > previous.endTime then previousTime = previous.endTime end
previous.animation:apply(skeleton, previousTime, previousTime, previous.loop, nil)
local alpha = current.mixTime / current.mixDuration * current.mix
if alpha >= 1 then
alpha = 1
current.previous = nil
end
current.animation:mix(skeleton, current.lastTime, time, loop, self.events, alpha)
end
local eventCount = #self.events
for ii = 1, eventCount, 1 do
local event = self.events[ii]
if current.onEvent then current.onEvent(i, event) end
if self.onEvent then self.onEvent(i, event) end
end
for ii = 1, eventCount, 1 do
table.remove(self.events)
end
-- Check if completed the animation or a loop iteration.
local complete
if current.loop then
complete = lastTime % endTime > time % endTime
else
complete = lastTime < endTime and time >= endTime
end
if complete then
local count = math.floor(time / endTime)
if current.onComplete then current.onComplete(i, count) end
if self.onComplete then self.onComplete(i, count) end
end
current.lastTime = current.time
end
end
end
function self:clearTracks ()
for i,current in pairs(self.tracks) do
self.clearTrack(i)
end
self.tracks = {}
self.trackCount = 0
end
function self:clearTrack (trackIndex)
local current = self.tracks[trackIndex]
if not current then return end
if current.onEnd then current.onEnd(trackIndex) end
if self.onEnd then self.onEnd(trackIndex) end
self.tracks[trackIndex] = nil
if trackIndex == self.trackCount - 1 then
self.trackCount = self.trackCount - 1
end
end
function self:setAnimationByName (trackIndex, animationName, loop)
local animation = self.data.skeletonData:findAnimation(animationName)
if not animation then error("Animation not found: " .. animationName) end
return self:setAnimation(trackIndex, animation, loop)
end
-- Set the current animation. Any queued animations are cleared.
function self:setAnimation (trackIndex, animation, loop)
local entry = AnimationState.TrackEntry.new()
entry.animation = animation
entry.loop = loop
entry.endTime = animation.duration
setCurrent(trackIndex, entry)
return entry
end
function self:addAnimationByName (trackIndex, animationName, loop, delay)
local animation = self.data.skeletonData:findAnimation(animationName)
if not animation then error("Animation not found: " .. animationName) end
return self:addAnimation(trackIndex, animation, loop, delay)
end
-- Adds an animation to be played delay seconds after the current or last queued animation.
-- @param delay May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay.
function self:addAnimation (trackIndex, animation, loop, delay)
local entry = AnimationState.TrackEntry.new()
entry.animation = animation
entry.loop = loop
entry.endTime = animation.duration
local last = self.tracks[trackIndex]
if last then
while (last.next) do
last = last.next
end
last.next = entry
else
self.tracks[trackIndex] = entry
end
delay = delay or 0
if delay <= 0 then
if last then
delay = delay + last.endTime - self.data:getMix(last.animation.name, animation.name)
else
delay = 0
end
end
entry.delay = delay
return entry
end
-- May return nil.
function self:getCurrent (trackIndex)
return self.tracks[trackIndex]
end
return self
end
AnimationState.TrackEntry = {}
function AnimationState.TrackEntry.new (data)
local self = {
next = nil, previous = nil,
animation = nil,
loop = false,
delay = 0, time = 0, lastTime = -1, endTime = 0,
timeScale = 1,
mixTime = 0, mixDuration = 0, mix = 0,
onStart = nil, onEnd = nil, onComplete = nil, onEvent = nil
}
return self
end
return AnimationState