diff --git a/spine-lua/Animation.lua b/spine-lua/Animation.lua index 84b510fde..6991d95c8 100644 --- a/spine-lua/Animation.lua +++ b/spine-lua/Animation.lua @@ -85,6 +85,7 @@ local function binarySearch (values, target, step) current = math.floor((low + high) / 2) end end +Animation.binarySearch = binarySearch local function binarySearch1 (values, target) local low = 0 diff --git a/spine-lua/AnimationState.lua b/spine-lua/AnimationState.lua index e18e3a698..3d89e0c93 100644 --- a/spine-lua/AnimationState.lua +++ b/spine-lua/AnimationState.lua @@ -37,6 +37,12 @@ local math_min = math.min local math_abs = math.abs local math_signum = utils.signum +local function zlen(array) + return #array + 1 +end + +local EMPTY_ANIMATION = Animation.new("", {}, 0) + local EventType = { start = 0, interrupt = 1, @@ -371,6 +377,400 @@ function AnimationState:applyMixingFrom (entry, skeleton) return mix end --- CONTINUE WITH applyRotateTimeline here +function AnimationState:applyRotateTimeline (timeline, skeleton, time, alpha, setupPose, timelinesRotation, i, firstFrame) + if alpha == 1 then + timeline:apply(skeleton, 0, time, nil, 1, setupPose, false) + return + end + + local rotateTimeline = timeline + local frames = rotateTimeline.frames + if time < frames[0] then return end -- Time is before first frame. + + local bone = skeleton.bones[rotateTimeline.boneIndex] + + local r2 = 0 + if time >= frames[zlen(frames) - Animation.RotateTimeline.ENTRIES] then -- Time is after last frame. + r2 = bone.data.rotation + frames[zlen(frames) + PREV_ROTATION] + else + -- Interpolate between the previous frame and the current frame. + local frame = Animation.binarySearch(frames, time, ENTRIES) + local prevRotation = frames[frame + PREV_ROTATION] + local frameTime = frames[frame] + local percent = rotateTimeline:getCurvePercent(math_floor(frame / 2) - 1, + 1 - (time - frameTime) / (frames[frame + PREV_TIME] - frameTime)) + + r2 = frames[frame + ROTATION] - prevRotation + r2 = r2 - (16384 - math_floor(16384.499999999996 - r2 / 360)) * 360 + r2 = prevRotation + r2 * percent + bone.data.rotation + r2 = r2 - (16384 - math_floor(16384.499999999996 - r2 / 360)) * 360 + end + + -- Mix between rotations using the direction of the shortest route on the first frame while detecting crosses. + local r1 = bone.rotation + if setupPose then r1 = bone.data.rotation end + local total = 0 + local diff = r2 - r1 + if diff == 0 then + if firstFrame then + timelinesRotation[i] = 0 + total = 0 + else + total = timelinesRotation[i] + end + else + diff = diff - (16384 - math_floor(16384.499999999996 - diff / 360)) * 360 + local lastTotal = 0 + local lastDiff = 0 + if firstFrame then + lastTotal = 0 + lastDiff = diff + else + lastTotal = timelinesRotation[i] -- Angle and direction of mix, including loops. + lastDiff = timelinesRotation[i + 1] -- Difference between bones. + end + local current = diff > 0 + local dir = lastTotal >= 0 + -- Detect cross at 0 (not 180). + if math_signum(lastDiff) ~= math_signum(diff) and math_abs(lastDiff) <= 90 then + -- A cross after a 360 rotation is a loop. + if math_abs(lastTotal) > 180 then lastTotal = lastTotal + 360 * math_signum(lastTotal) end + dir = current + end + total = diff + lastTotal - math_ceil(lastTotal / 360 - 0.5) * 360 -- FIXME used to be %360, store loops as part of lastTotal. + if dir ~= current then total = total + 360 * Math.signum(lastTotal) end + timelinesRotation[i] = total + end + timelinesRotation[i + 1] = diff + r1 = r1 + total * alpha + bone.rotation = r1 - (16384 - math_floor(16384.499999999996 - r1 / 360)) * 360 +end + +function AnimationState:queueEvents (entry, animationTime) + local animationStart = entry.animationStart + local animationEnd = entry.animationEnd + local duration = animationEnd - animationStart + local trackLastWrapped = entry.trackLast % duration + + -- Queue events before complete. + local events = self.events + local queue = self.queue + local i = 1 + local n = #events + while i <= n do + local event = events[i] + if event.time < trackLastWrapped then break end + if not (event.time > animationEnd) then -- Discard events outside animation start/end. + queue:event(entry, event) + end + i = i + 1 + end + + -- Queue complete if completed a loop iteration or the animation. + local queueComplete = false + if entry.loop then + queueComplete = (trackLastWrapped > entry.trackTime % duration) + else + queueComplete = (animationTime >= animationEnd and entry.animationLast < animationEnd) + end + if queueComplete then + queue:complete(entry) + end + + -- Queue events after complete. + while i <= n do + local event = events[i] + if not (event.time < animationStart) then --// Discard events outside animation start/end. + queue.event(entry, event) + end + i = i + 1 + end + events = {} +end + +function AnimationState:clearTracks () + local queue = self.queue + local tracks = self.tracks + queue.drainDisabled = true; + for i,track in pairs(tracks) do + self:clearTrack(i) + end + tracks = {} + queue.drainDisabled = false; + queue:drain(); +end + +function AnimationState:clearTrack (trackIndex) + local tracks = self.tracks + local queue = self.queue + local current = tracks[trackIndex] + if current == nil then return end + + queue:_end(current) + + self:disposeNext(current) + + local entry = current; + while (true) do + local from = entry.mixingFrom + if from == nil then break end + queue:_end(from) + entry.mixingFrom = nil + entry = from + end + + tracks[current.trackIndex] = nil + + queue:drain() +end + +function AnimationState:setCurrent (index, current) + local from = self:expandToIndex(index) + local tracks = self.tracks + local queue = self.queue + tracks[index] = current + + if from then + queue:interrupt(from) + current.mixingFrom = from + current.mixTime = 0 + + -- If not completely mixed in, set mixAlpha so mixing out happens from current mix to zero. + if from.mixingFrom then current.mixAlpha = current.mixAlpha * math_min(from.mixTime / from.mixDuration, 1) end + end + + queue:start(current) +end + +function AnimationState:setAnimationByName (trackIndex, animationName, loop) + local animation = self.data.skeletonData:findAnimation(animationName) + if not animation then error("Animation not found: " .. animationName, 2) end + return self:setAnimation(trackIndex, animation, loop) +end + +function AnimationState:setAnimation (trackIndex, animation, loop) + if not animation then error("animation cannot be null.") end + local current = self:expandToIndex(trackIndex) + local queue = self.queue + if current then + if current.nextTrackLast == -1 then + -- Don't mix from an entry that was never applied. + tracks[trackIndex] = nil + queue:interrupt(current) + queue:_end(current) + self:disposeNext(current) + current = nil + else + self:disposeNext(current) + end + end + local entry = self:trackEntry(trackIndex, animation, loop, current) + self:setCurrent(trackIndex, entry) + queue:drain() + return entry +end + +function AnimationState:addAnimationByName (trackIndex, animationName, loop, delay) + local animation = data.skeletonData:findAnimation(animationName) + if not animation then error("Animation not found: " + animationName) end + return self:addAnimation(trackIndex, animation, loop, delay) +end + +function AnimationState:addAnimation (trackIndex, animationName, loop, delay) + if not nimation then error("animation cannot be null.") end + + local last = self:expandToIndex(trackIndex) + if last then + while last.next do + last = last.next + end + end + + local entry = self:trackEntry(trackIndex, animation, loop, last) + local queue = self.queue + + if not last then + self:setCurrent(trackIndex, entry) + queue:drain() + else + last.next = entry + if delay <= 0 then + local duration = last.animationEnd - last.animationStart + if duration ~= 0 then + delay = delay + duration * (1 + math_floor(last.trackTime / duration)) - data:getMix(last.animation, animation) + else + delay = 0 + end + end + end + + entry.delay = delay + return entry +end + +function AnimationState:setEmptyAnimation (trackIndex, mixDuration) + local entry = self:setAnimation(trackIndex, EMPTY_ANIMATION, false) + entry.mixDuration = mixDuration + entry.trackEnd = mixDuration + return entry +end + +function AnimationState:addEmptyAnimation (trackIndex, mixDuration, delay) + if delay <= 0 then delay = delay - mixDuration end + local entry = self:addAnimation(trackIndex, EMPTY_ANIMATION, false, delay) + entry.mixDuration = mixDuration + entry.trackEnd = mixDuration + return entry +end + +function AnimationState:setEmptyAnimations (mixDuration) + local queue = self.queue + queue.drainDisabled = true + for i,current in pairs(self.tracks) do + if current then self:setEmptyAnimation(current.trackIndex, mixDuration) end + end + queue.drainDisabled = false + queue:drain() +end + +function AnimationState:expandToIndex (index) + return self.tracks[index] +end + +function AnimationState:trackEntry (trackIndex, animation, loop, last) + local data = self.data + local entry = TrackEntry.new() + entry.trackIndex = trackIndex + entry.animation = animation + entry.loop = loop + + entry.eventThreshold = 0 + entry.attachmentThreshold = 0 + entry.drawOrderThreshold = 0 + + entry.animationStart = 0 + entry.animationEnd = animation.duration + entry.animationLast = -1 + entry.nextAnimationLast = -1 + + entry.delay = 0 + entry.trackTime = 0 + entry.trackLast = -1 + entry.nextTrackLast = -1 + if loop then + entry.trackEnd = 999999999 + else + entry.trackEnd = entry.animationEnd + end + entry.timeScale = 1 + + entry.alpha = 1 + entry.mixAlpha = 1 + entry.mixTime = 0 + if not last then + entry.mixDuration = 0 + else + entry.mixDuration = data:getMix(last.animation, animation) + end + return entry +end + +function AnimationState:disposeNext (entry) + local _next = entry.next + local queue = self.queue + while _next do + queue:dispose(_next) + _next = next.next + end + entry.next = nil +end + +function AnimationState:animationsChanged () + self.animationsChanged = false; + + self.propertyIDs = {} + local propertyIDs = self.propertyIDs; + + -- need to get the highest index cause Lua is funny + local highest = -1 + for i,entry in pairs(tracks) do + if i > highest then highest = i end + end + + -- Set timelinesFirst for all entries, from lowest track to highest. + local i = 1 + local n = highest + local tracks = self.tracks + while i <= n do + local entry = tracks[i] + if entry then + self:setTimelinesFirst(entry); + i = i + 1 + break; + end + i = i + 1 + end + while i <= n do + local entry = tracks[i] + if entry then self:checkTimelinesFirst(entry) end + i = i + 1 + end +end + +function AnimationState:setTimelinesFirst (entry) + if entry.mixingFrom then + self:setTimelinesFirst(entry.mixingFrom) + self:checkTimelinesUsage(entry, entry.timelinesFirst) + return + end + local propertyIDs = self.propertyIDs + local n = #entry.animation.timelines + local timelines = entry.animation.timelines + entry.timelinesFirst = {} + local usage = entry.timelinesFirst; + local i = 1 + while i <= n do + local id = "" .. timelines[i]:getPropertyId() + propertyIDs[id] = id + usage[i] = true; + i = i + 1 + end +end + +function AnimationState:checkTimlinesFirst (entry) + if entry.mixingFrom then self:checkTimelinesFirst(entry.mixingFrom) end + self:checkTimelinesUsage(entry, entry.timelinesFirst) +end + +function AnimationState:checkTimelinesUsage (entry, usageArray) + local propertyIDs = self.propertyIDs + local n = #entry.animation.timelines + local timelines = entry.animation.timelines + local usage = usageArray + while i <= n do + local id = "" .. timelines[i]:getPropertyId() + local contained = propertyIDs[id] == id + propertyIDs[id] = id + usage[i] = not contained + i = i + 1 + end +end + +function AnimationState:getCurrent (trackIndex) + return self.tracks[trackIndex] +end + +function AnimationState:clearListeners () + self.onStart = nil + self.onInterrupt = nil + self.onEnd = nil + self.onComplete = nil + self.onDispose = nil + self.onEvent = nil +end + +function AnimationState:clearListenerNotificatin () + self.queue:clear() +end return AnimationState