UNPKG

@pixi-spine/runtime-3.8

Version:

Pixi runtime for spine 3.8 models

1 lines 97.1 kB
{"version":3,"file":"AnimationState.mjs","sources":["../../src/core/AnimationState.ts"],"sourcesContent":["import { IAnimationState, IAnimationStateListener, ITrackEntry, MixBlend, MixDirection, MathUtils, Pool, IntSet, Utils } from '@pixi-spine/base';\nimport { Animation, AttachmentTimeline, DrawOrderTimeline, EventTimeline, RotateTimeline, Timeline } from './Animation';\nimport type { AnimationStateData } from './AnimationStateData';\nimport type { Event } from './Event';\nimport type { Skeleton } from './Skeleton';\nimport type { Slot } from './Slot';\n\n/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies\n * multiple animations on top of each other (layering).\n *\n * See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide. */\n/**\n * @public\n */\nexport class AnimationState implements IAnimationState<AnimationStateData> {\n static emptyAnimation = new Animation('<empty>', [], 0);\n\n /** 1. A previously applied timeline has set this property.\n *\n * Result: Mix from the current pose to the timeline pose. */\n static SUBSEQUENT = 0;\n /** 1. This is the first timeline to set this property.\n * 2. The next track entry applied after this one does not have a timeline to set this property.\n *\n * Result: Mix from the setup pose to the timeline pose. */\n static FIRST = 1;\n /** 1) A previously applied timeline has set this property.<br>\n * 2) The next track entry to be applied does have a timeline to set this property.<br>\n * 3) The next track entry after that one does not have a timeline to set this property.<br>\n * Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids \"dipping\" when crossfading\n * animations that key the same property. A subsequent timeline will set this property using a mix. */\n static HOLD_SUBSEQUENT = 2;\n /** 1) This is the first timeline to set this property.<br>\n * 2) The next track entry to be applied does have a timeline to set this property.<br>\n * 3) The next track entry after that one does not have a timeline to set this property.<br>\n * Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids \"dipping\" when crossfading animations\n * that key the same property. A subsequent timeline will set this property using a mix. */\n static HOLD_FIRST = 3;\n /** 1. This is the first timeline to set this property.\n * 2. The next track entry to be applied does have a timeline to set this property.\n * 3. The next track entry after that one does have a timeline to set this property.\n * 4. timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.\n *\n * Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than\n * 2 track entries in a row have a timeline that sets the same property.\n *\n * Eg, A -> B -> C -> D where A, B, and C have a timeline setting same property, but D does not. When A is applied, to avoid\n * \"dipping\" A is not mixed out, however D (the first entry that doesn't set the property) mixing in is used to mix out A\n * (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into\n * place. */\n static HOLD_MIX = 4;\n\n static SETUP = 1;\n static CURRENT = 2;\n\n /** The AnimationStateData to look up mix durations. */\n data: AnimationStateData;\n\n /** The list of tracks that currently have animations, which may contain null entries. */\n tracks = new Array<TrackEntry>();\n\n /** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower\n * or faster. Defaults to 1.\n *\n * See TrackEntry {@link TrackEntry#timeScale} for affecting a single animation. */\n timeScale = 1;\n unkeyedState = 0;\n\n events = new Array<Event>();\n listeners = new Array<AnimationStateListener>();\n queue = new EventQueue(this);\n propertyIDs = new IntSet();\n animationsChanged = false;\n\n trackEntryPool = new Pool<TrackEntry>(() => new TrackEntry());\n\n constructor(data: AnimationStateData) {\n this.data = data;\n }\n\n /** Increments each track entry {@link TrackEntry#trackTime()}, setting queued animations as current if needed. */\n update(delta: number) {\n delta *= this.timeScale;\n const tracks = this.tracks;\n\n for (let i = 0, n = tracks.length; i < n; i++) {\n const current = tracks[i];\n\n if (current == null) continue;\n\n current.animationLast = current.nextAnimationLast;\n current.trackLast = current.nextTrackLast;\n\n let currentDelta = delta * current.timeScale;\n\n if (current.delay > 0) {\n current.delay -= currentDelta;\n if (current.delay > 0) continue;\n currentDelta = -current.delay;\n current.delay = 0;\n }\n\n let next = current.next;\n\n if (next != null) {\n // When the next entry's delay is passed, change to the next entry, preserving leftover time.\n const nextTime = current.trackLast - next.delay;\n\n if (nextTime >= 0) {\n next.delay = 0;\n next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale;\n current.trackTime += currentDelta;\n this.setCurrent(i, next, true);\n while (next.mixingFrom != null) {\n next.mixTime += delta;\n next = next.mixingFrom;\n }\n continue;\n }\n } else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {\n tracks[i] = null;\n this.queue.end(current);\n this.disposeNext(current);\n continue;\n }\n if (current.mixingFrom != null && this.updateMixingFrom(current, delta)) {\n // End mixing from entries once all have completed.\n let from = current.mixingFrom;\n\n current.mixingFrom = null;\n if (from != null) from.mixingTo = null;\n while (from != null) {\n this.queue.end(from);\n from = from.mixingFrom;\n }\n }\n\n current.trackTime += currentDelta;\n }\n\n this.queue.drain();\n }\n\n /** Returns true when all mixing from entries are complete. */\n updateMixingFrom(to: TrackEntry, delta: number): boolean {\n const from = to.mixingFrom;\n\n if (from == null) return true;\n\n const finished = this.updateMixingFrom(from, delta);\n\n from.animationLast = from.nextAnimationLast;\n from.trackLast = from.nextTrackLast;\n\n // Require mixTime > 0 to ensure the mixing from entry was applied at least once.\n if (to.mixTime > 0 && to.mixTime >= to.mixDuration) {\n // Require totalAlpha == 0 to ensure mixing is complete, unless mixDuration == 0 (the transition is a single frame).\n if (from.totalAlpha == 0 || to.mixDuration == 0) {\n to.mixingFrom = from.mixingFrom;\n if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;\n to.interruptAlpha = from.interruptAlpha;\n this.queue.end(from);\n }\n\n return finished;\n }\n\n from.trackTime += delta * from.timeScale;\n to.mixTime += delta;\n\n return false;\n }\n\n /** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the\n * animation state can be applied to multiple skeletons to pose them identically.\n * @returns True if any animations were applied. */\n apply(skeleton: Skeleton): boolean {\n if (skeleton == null) throw new Error('skeleton cannot be null.');\n if (this.animationsChanged) this._animationsChanged();\n\n const events = this.events;\n const tracks = this.tracks;\n let applied = false;\n\n for (let i = 0, n = tracks.length; i < n; i++) {\n const current = tracks[i];\n\n if (current == null || current.delay > 0) continue;\n applied = true;\n const blend: MixBlend = i == 0 ? MixBlend.first : current.mixBlend;\n\n // Apply mixing from entries first.\n let mix = current.alpha;\n\n if (current.mixingFrom != null) mix *= this.applyMixingFrom(current, skeleton, blend);\n else if (current.trackTime >= current.trackEnd && current.next == null) mix = 0;\n\n // Apply current entry.\n const animationLast = current.animationLast;\n const animationTime = current.getAnimationTime();\n const timelineCount = current.animation.timelines.length;\n const timelines = current.animation.timelines;\n\n if ((i == 0 && mix == 1) || blend == MixBlend.add) {\n for (let ii = 0; ii < timelineCount; ii++) {\n // Fixes issue #302 on IOS9 where mix, blend sometimes became undefined and caused assets\n // to sometimes stop rendering when using color correction, as their RGBA values become NaN.\n // (https://github.com/pixijs/pixi-spine/issues/302)\n Utils.webkit602BugfixHelper(mix, blend);\n const timeline = timelines[ii];\n\n if (timeline instanceof AttachmentTimeline) this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true);\n else timeline.apply(skeleton, animationLast, animationTime, events, mix, blend, MixDirection.mixIn);\n }\n } else {\n const timelineMode = current.timelineMode;\n\n const firstFrame = current.timelinesRotation.length == 0;\n\n if (firstFrame) Utils.setArraySize(current.timelinesRotation, timelineCount << 1, null);\n const timelinesRotation = current.timelinesRotation;\n\n for (let ii = 0; ii < timelineCount; ii++) {\n const timeline = timelines[ii];\n const timelineBlend = timelineMode[ii] == AnimationState.SUBSEQUENT ? blend : MixBlend.setup;\n\n if (timeline instanceof RotateTimeline) {\n this.applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, firstFrame);\n } else if (timeline instanceof AttachmentTimeline) {\n this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true);\n } else {\n // This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109\n Utils.webkit602BugfixHelper(mix, blend);\n timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, MixDirection.mixIn);\n }\n }\n }\n this.queueEvents(current, animationTime);\n events.length = 0;\n current.nextAnimationLast = animationTime;\n current.nextTrackLast = current.trackTime;\n }\n\n // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so\n // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or\n // the time is before the first key).\n const setupState = this.unkeyedState + AnimationState.SETUP;\n const slots = skeleton.slots;\n\n for (let i = 0, n = skeleton.slots.length; i < n; i++) {\n const slot = slots[i];\n\n if (slot.attachmentState == setupState) {\n const attachmentName = slot.data.attachmentName;\n\n slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));\n }\n }\n this.unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot.\n\n this.queue.drain();\n\n return applied;\n }\n\n applyMixingFrom(to: TrackEntry, skeleton: Skeleton, blend: MixBlend) {\n const from = to.mixingFrom;\n\n if (from.mixingFrom != null) this.applyMixingFrom(from, skeleton, blend);\n\n let mix = 0;\n\n if (to.mixDuration == 0) {\n // Single frame mix to undo mixingFrom changes.\n mix = 1;\n if (blend == MixBlend.first) blend = MixBlend.setup;\n } else {\n mix = to.mixTime / to.mixDuration;\n if (mix > 1) mix = 1;\n if (blend != MixBlend.first) blend = from.mixBlend;\n }\n\n const events = mix < from.eventThreshold ? this.events : null;\n const attachments = mix < from.attachmentThreshold;\n const drawOrder = mix < from.drawOrderThreshold;\n const animationLast = from.animationLast;\n const animationTime = from.getAnimationTime();\n const timelineCount = from.animation.timelines.length;\n const timelines = from.animation.timelines;\n const alphaHold = from.alpha * to.interruptAlpha;\n const alphaMix = alphaHold * (1 - mix);\n\n if (blend == MixBlend.add) {\n for (let i = 0; i < timelineCount; i++) timelines[i].apply(skeleton, animationLast, animationTime, events, alphaMix, blend, MixDirection.mixOut);\n } else {\n const timelineMode = from.timelineMode;\n const timelineHoldMix = from.timelineHoldMix;\n\n const firstFrame = from.timelinesRotation.length == 0;\n\n if (firstFrame) Utils.setArraySize(from.timelinesRotation, timelineCount << 1, null);\n const timelinesRotation = from.timelinesRotation;\n\n from.totalAlpha = 0;\n for (let i = 0; i < timelineCount; i++) {\n const timeline = timelines[i];\n let direction = MixDirection.mixOut;\n let timelineBlend: MixBlend;\n let alpha = 0;\n\n switch (timelineMode[i]) {\n case AnimationState.SUBSEQUENT:\n if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;\n timelineBlend = blend;\n alpha = alphaMix;\n break;\n case AnimationState.FIRST:\n timelineBlend = MixBlend.setup;\n alpha = alphaMix;\n break;\n case AnimationState.HOLD_SUBSEQUENT:\n timelineBlend = blend;\n alpha = alphaHold;\n break;\n case AnimationState.HOLD_FIRST:\n timelineBlend = MixBlend.setup;\n alpha = alphaHold;\n break;\n default:\n timelineBlend = MixBlend.setup;\n const holdMix = timelineHoldMix[i];\n\n alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);\n break;\n }\n from.totalAlpha += alpha;\n\n if (timeline instanceof RotateTimeline) this.applyRotateTimeline(timeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation, i << 1, firstFrame);\n else if (timeline instanceof AttachmentTimeline) this.applyAttachmentTimeline(timeline, skeleton, animationTime, timelineBlend, attachments);\n else {\n // This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109\n Utils.webkit602BugfixHelper(alpha, blend);\n if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend == MixBlend.setup) direction = MixDirection.mixIn;\n timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction);\n }\n }\n }\n\n if (to.mixDuration > 0) this.queueEvents(from, animationTime);\n this.events.length = 0;\n from.nextAnimationLast = animationTime;\n from.nextTrackLast = from.trackTime;\n\n return mix;\n }\n\n applyAttachmentTimeline(timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, attachments: boolean) {\n const slot = skeleton.slots[timeline.slotIndex];\n\n if (!slot.bone.active) return;\n\n const frames = timeline.frames;\n\n if (time < frames[0]) {\n // Time is before first frame.\n if (blend == MixBlend.setup || blend == MixBlend.first) this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);\n } else {\n let frameIndex;\n\n if (time >= frames[frames.length - 1])\n // Time is after last frame.\n frameIndex = frames.length - 1;\n else frameIndex = Animation.binarySearch(frames, time) - 1;\n this.setAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments);\n }\n\n // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later.\n if (slot.attachmentState <= this.unkeyedState) slot.attachmentState = this.unkeyedState + AnimationState.SETUP;\n }\n\n setAttachment(skeleton: Skeleton, slot: Slot, attachmentName: string, attachments: boolean) {\n slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));\n if (attachments) slot.attachmentState = this.unkeyedState + AnimationState.CURRENT;\n }\n\n applyRotateTimeline(timeline: Timeline, skeleton: Skeleton, time: number, alpha: number, blend: MixBlend, timelinesRotation: Array<number>, i: number, firstFrame: boolean) {\n if (firstFrame) timelinesRotation[i] = 0;\n\n if (alpha == 1) {\n timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn);\n\n return;\n }\n\n const rotateTimeline = timeline as RotateTimeline;\n const frames = rotateTimeline.frames;\n const bone = skeleton.bones[rotateTimeline.boneIndex];\n\n if (!bone.active) return;\n let r1 = 0;\n let r2 = 0;\n\n if (time < frames[0]) {\n switch (blend) {\n case MixBlend.setup:\n bone.rotation = bone.data.rotation;\n default:\n return;\n case MixBlend.first:\n r1 = bone.rotation;\n r2 = bone.data.rotation;\n }\n } else {\n r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;\n if (time >= frames[frames.length - RotateTimeline.ENTRIES])\n // Time is after last frame.\n r2 = bone.data.rotation + frames[frames.length + RotateTimeline.PREV_ROTATION];\n else {\n // Interpolate between the previous frame and the current frame.\n const frame = Animation.binarySearch(frames, time, RotateTimeline.ENTRIES);\n const prevRotation = frames[frame + RotateTimeline.PREV_ROTATION];\n const frameTime = frames[frame];\n const percent = rotateTimeline.getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + RotateTimeline.PREV_TIME] - frameTime));\n\n r2 = frames[frame + RotateTimeline.ROTATION] - prevRotation;\n r2 -= (16384 - ((16384.499999999996 - r2 / 360) | 0)) * 360;\n r2 = prevRotation + r2 * percent + bone.data.rotation;\n r2 -= (16384 - ((16384.499999999996 - r2 / 360) | 0)) * 360;\n }\n }\n\n // Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.\n let total = 0;\n let diff = r2 - r1;\n\n diff -= (16384 - ((16384.499999999996 - diff / 360) | 0)) * 360;\n if (diff == 0) {\n total = timelinesRotation[i];\n } else {\n let lastTotal = 0;\n let lastDiff = 0;\n\n if (firstFrame) {\n lastTotal = 0;\n lastDiff = diff;\n } else {\n lastTotal = timelinesRotation[i]; // Angle and direction of mix, including loops.\n lastDiff = timelinesRotation[i + 1]; // Difference between bones.\n }\n const current = diff > 0;\n let dir = lastTotal >= 0;\n // Detect cross at 0 (not 180).\n\n if (MathUtils.signum(lastDiff) != MathUtils.signum(diff) && Math.abs(lastDiff) <= 90) {\n // A cross after a 360 rotation is a loop.\n if (Math.abs(lastTotal) > 180) lastTotal += 360 * MathUtils.signum(lastTotal);\n dir = current;\n }\n total = diff + lastTotal - (lastTotal % 360); // Store loops as part of lastTotal.\n if (dir != current) total += 360 * MathUtils.signum(lastTotal);\n timelinesRotation[i] = total;\n }\n timelinesRotation[i + 1] = diff;\n r1 += total * alpha;\n bone.rotation = r1 - (16384 - ((16384.499999999996 - r1 / 360) | 0)) * 360;\n }\n\n queueEvents(entry: TrackEntry, animationTime: number) {\n const animationStart = entry.animationStart;\n const animationEnd = entry.animationEnd;\n const duration = animationEnd - animationStart;\n const trackLastWrapped = entry.trackLast % duration;\n\n // Queue events before complete.\n const events = this.events;\n let i = 0;\n const n = events.length;\n\n for (; i < n; i++) {\n const event = events[i];\n\n if (event.time < trackLastWrapped) break;\n if (event.time > animationEnd) continue; // Discard events outside animation start/end.\n this.queue.event(entry, event);\n }\n\n // Queue complete if completed a loop iteration or the animation.\n let complete = false;\n\n if (entry.loop) complete = duration == 0 || trackLastWrapped > entry.trackTime % duration;\n else complete = animationTime >= animationEnd && entry.animationLast < animationEnd;\n if (complete) this.queue.complete(entry);\n\n // Queue events after complete.\n for (; i < n; i++) {\n const event = events[i];\n\n if (event.time < animationStart) continue; // Discard events outside animation start/end.\n this.queue.event(entry, events[i]);\n }\n }\n\n /** Removes all animations from all tracks, leaving skeletons in their current pose.\n *\n * It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose,\n * rather than leaving them in their current pose. */\n clearTracks() {\n const oldDrainDisabled = this.queue.drainDisabled;\n\n this.queue.drainDisabled = true;\n for (let i = 0, n = this.tracks.length; i < n; i++) this.clearTrack(i);\n this.tracks.length = 0;\n this.queue.drainDisabled = oldDrainDisabled;\n this.queue.drain();\n }\n\n /** Removes all animations from the track, leaving skeletons in their current pose.\n *\n * It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose,\n * rather than leaving them in their current pose. */\n clearTrack(trackIndex: number) {\n if (trackIndex >= this.tracks.length) return;\n const current = this.tracks[trackIndex];\n\n if (current == null) return;\n\n this.queue.end(current);\n\n this.disposeNext(current);\n\n let entry = current;\n\n while (true) {\n const from = entry.mixingFrom;\n\n if (from == null) break;\n this.queue.end(from);\n entry.mixingFrom = null;\n entry.mixingTo = null;\n entry = from;\n }\n\n this.tracks[current.trackIndex] = null;\n\n this.queue.drain();\n }\n\n setCurrent(index: number, current: TrackEntry, interrupt: boolean) {\n const from = this.expandToIndex(index);\n\n this.tracks[index] = current;\n\n if (from != null) {\n if (interrupt) this.queue.interrupt(from);\n current.mixingFrom = from;\n from.mixingTo = current;\n current.mixTime = 0;\n\n // Store the interrupted mix percentage.\n if (from.mixingFrom != null && from.mixDuration > 0) current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration);\n\n from.timelinesRotation.length = 0; // Reset rotation for mixing out, in case entry was mixed in.\n }\n\n this.queue.start(current);\n }\n\n /** Sets an animation by name.\n *\n * {@link #setAnimationWith(}. */\n setAnimation(trackIndex: number, animationName: string, loop: boolean) {\n const animation = this.data.skeletonData.findAnimation(animationName);\n\n if (animation == null) throw new Error(`Animation not found: ${animationName}`);\n\n return this.setAnimationWith(trackIndex, animation, loop);\n }\n\n /** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never\n * applied to a skeleton, it is replaced (not mixed from).\n * @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its\n * duration. In either case {@link TrackEntry#trackEnd} determines when the track is cleared.\n * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept\n * after the {@link AnimationStateListener#dispose()} event occurs. */\n setAnimationWith(trackIndex: number, animation: Animation, loop: boolean) {\n if (animation == null) throw new Error('animation cannot be null.');\n let interrupt = true;\n let current = this.expandToIndex(trackIndex);\n\n if (current != null) {\n if (current.nextTrackLast == -1) {\n // Don't mix from an entry that was never applied.\n this.tracks[trackIndex] = current.mixingFrom;\n this.queue.interrupt(current);\n this.queue.end(current);\n this.disposeNext(current);\n current = current.mixingFrom;\n interrupt = false;\n } else this.disposeNext(current);\n }\n const entry = this.trackEntry(trackIndex, animation, loop, current);\n\n this.setCurrent(trackIndex, entry, interrupt);\n this.queue.drain();\n\n return entry;\n }\n\n /** Queues an animation by name.\n *\n * See {@link #addAnimationWith()}. */\n addAnimation(trackIndex: number, animationName: string, loop: boolean, delay: number) {\n const animation = this.data.skeletonData.findAnimation(animationName);\n\n if (animation == null) throw new Error(`Animation not found: ${animationName}`);\n\n return this.addAnimationWith(trackIndex, animation, loop, delay);\n }\n\n /** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is\n * equivalent to calling {@link #setAnimationWith()}.\n * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry\n * minus any mix duration (from the {@link AnimationStateData}) plus the specified `delay` (ie the mix\n * ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the\n * previous entry is looping, its next loop completion is used instead of its duration.\n * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept\n * after the {@link AnimationStateListener#dispose()} event occurs. */\n addAnimationWith(trackIndex: number, animation: Animation, loop: boolean, delay: number) {\n if (animation == null) throw new Error('animation cannot be null.');\n\n let last = this.expandToIndex(trackIndex);\n\n if (last != null) {\n while (last.next != null) last = last.next;\n }\n\n const entry = this.trackEntry(trackIndex, animation, loop, last);\n\n if (last == null) {\n this.setCurrent(trackIndex, entry, true);\n this.queue.drain();\n } else {\n last.next = entry;\n if (delay <= 0) {\n const duration = last.animationEnd - last.animationStart;\n\n if (duration != 0) {\n if (last.loop) delay += duration * (1 + ((last.trackTime / duration) | 0));\n else delay += Math.max(duration, last.trackTime);\n delay -= this.data.getMix(last.animation, animation);\n } else delay = last.trackTime;\n }\n }\n\n entry.delay = delay;\n\n return entry;\n }\n\n /** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's\n * {@link TrackEntry#mixduration}. An empty animation has no timelines and serves as a placeholder for mixing in or out.\n *\n * Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation()},\n * {@link #setEmptyAnimations()}, or {@link #addEmptyAnimation()}. Mixing to an empty animation causes\n * the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation\n * transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of\n * 0 still mixes out over one frame.\n *\n * Mixing in is done by first setting an empty animation, then adding an animation using\n * {@link #addAnimation()} and on the returned track entry, set the\n * {@link TrackEntry#setMixDuration()}. Mixing from an empty animation causes the new animation to be applied more and\n * more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the\n * setup pose value if no lower tracks key the property to the value keyed in the new animation. */\n setEmptyAnimation(trackIndex: number, mixDuration: number) {\n const entry = this.setAnimationWith(trackIndex, AnimationState.emptyAnimation, false);\n\n entry.mixDuration = mixDuration;\n entry.trackEnd = mixDuration;\n\n return entry;\n }\n\n /** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's\n * {@link TrackEntry#mixDuration}. If the track is empty, it is equivalent to calling\n * {@link #setEmptyAnimation()}.\n *\n * See {@link #setEmptyAnimation()}.\n * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry\n * minus any mix duration plus the specified `delay` (ie the mix ends at (`delay` = 0) or\n * before (`delay` < 0) the previous track entry duration). If the previous entry is looping, its next\n * loop completion is used instead of its duration.\n * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept\n * after the {@link AnimationStateListener#dispose()} event occurs. */\n addEmptyAnimation(trackIndex: number, mixDuration: number, delay: number) {\n if (delay <= 0) delay -= mixDuration;\n const entry = this.addAnimationWith(trackIndex, AnimationState.emptyAnimation, false, delay);\n\n entry.mixDuration = mixDuration;\n entry.trackEnd = mixDuration;\n\n return entry;\n }\n\n /** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix\n * duration. */\n setEmptyAnimations(mixDuration: number) {\n const oldDrainDisabled = this.queue.drainDisabled;\n\n this.queue.drainDisabled = true;\n for (let i = 0, n = this.tracks.length; i < n; i++) {\n const current = this.tracks[i];\n\n if (current != null) this.setEmptyAnimation(current.trackIndex, mixDuration);\n }\n this.queue.drainDisabled = oldDrainDisabled;\n this.queue.drain();\n }\n\n expandToIndex(index: number) {\n if (index < this.tracks.length) return this.tracks[index];\n Utils.ensureArrayCapacity(this.tracks, index + 1, null);\n this.tracks.length = index + 1;\n\n return null;\n }\n\n /** @param last May be null. */\n trackEntry(trackIndex: number, animation: Animation, loop: boolean, last: TrackEntry) {\n const entry = this.trackEntryPool.obtain();\n\n entry.trackIndex = trackIndex;\n entry.animation = animation;\n entry.loop = loop;\n entry.holdPrevious = false;\n\n entry.eventThreshold = 0;\n entry.attachmentThreshold = 0;\n entry.drawOrderThreshold = 0;\n\n entry.animationStart = 0;\n entry.animationEnd = animation.duration;\n entry.animationLast = -1;\n entry.nextAnimationLast = -1;\n\n entry.delay = 0;\n entry.trackTime = 0;\n entry.trackLast = -1;\n entry.nextTrackLast = -1;\n entry.trackEnd = Number.MAX_VALUE;\n entry.timeScale = 1;\n\n entry.alpha = 1;\n entry.interruptAlpha = 1;\n entry.mixTime = 0;\n entry.mixDuration = last == null ? 0 : this.data.getMix(last.animation, animation);\n entry.mixBlend = MixBlend.replace;\n\n return entry;\n }\n\n disposeNext(entry: TrackEntry) {\n let next = entry.next;\n\n while (next != null) {\n this.queue.dispose(next);\n next = next.next;\n }\n entry.next = null;\n }\n\n _animationsChanged() {\n this.animationsChanged = false;\n\n this.propertyIDs.clear();\n\n for (let i = 0, n = this.tracks.length; i < n; i++) {\n let entry = this.tracks[i];\n\n if (entry == null) continue;\n while (entry.mixingFrom != null) entry = entry.mixingFrom;\n\n do {\n if (entry.mixingFrom == null || entry.mixBlend != MixBlend.add) this.computeHold(entry);\n entry = entry.mixingTo;\n } while (entry != null);\n }\n }\n\n computeHold(entry: TrackEntry) {\n const to = entry.mixingTo;\n const timelines = entry.animation.timelines;\n const timelinesCount = entry.animation.timelines.length;\n const timelineMode = Utils.setArraySize(entry.timelineMode, timelinesCount);\n\n entry.timelineHoldMix.length = 0;\n const timelineDipMix = Utils.setArraySize(entry.timelineHoldMix, timelinesCount);\n const propertyIDs = this.propertyIDs;\n\n if (to != null && to.holdPrevious) {\n for (let i = 0; i < timelinesCount; i++) {\n timelineMode[i] = propertyIDs.add(timelines[i].getPropertyId()) ? AnimationState.HOLD_FIRST : AnimationState.HOLD_SUBSEQUENT;\n }\n\n return;\n }\n\n // eslint-disable-next-line no-restricted-syntax, no-labels\n outer: for (let i = 0; i < timelinesCount; i++) {\n const timeline = timelines[i];\n const id = timeline.getPropertyId();\n\n if (!propertyIDs.add(id)) timelineMode[i] = AnimationState.SUBSEQUENT;\n else if (\n to == null ||\n timeline instanceof AttachmentTimeline ||\n timeline instanceof DrawOrderTimeline ||\n timeline instanceof EventTimeline ||\n !to.animation.hasTimeline(id)\n ) {\n timelineMode[i] = AnimationState.FIRST;\n } else {\n for (let next = to.mixingTo; next != null; next = next.mixingTo) {\n if (next.animation.hasTimeline(id)) continue;\n if (entry.mixDuration > 0) {\n timelineMode[i] = AnimationState.HOLD_MIX;\n timelineDipMix[i] = next;\n // eslint-disable-next-line no-labels\n continue outer;\n }\n break;\n }\n timelineMode[i] = AnimationState.HOLD_FIRST;\n }\n }\n }\n\n /** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */\n getCurrent(trackIndex: number) {\n if (trackIndex >= this.tracks.length) return null;\n\n return this.tracks[trackIndex];\n }\n\n /** Adds a listener to receive events for all track entries. */\n addListener(listener: AnimationStateListener) {\n if (listener == null) throw new Error('listener cannot be null.');\n this.listeners.push(listener);\n }\n\n /** Removes the listener added with {@link #addListener()}. */\n removeListener(listener: AnimationStateListener) {\n const index = this.listeners.indexOf(listener);\n\n if (index >= 0) this.listeners.splice(index, 1);\n }\n\n /** Removes all listeners added with {@link #addListener()}. */\n clearListeners() {\n this.listeners.length = 0;\n }\n\n /** Discards all listener notifications that have not yet been delivered. This can be useful to call from an\n * {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery\n * are not wanted because new animations are being set. */\n clearListenerNotifications() {\n this.queue.clear();\n }\n\n // deprecated stuff\n onComplete: (trackIndex: number, loopCount: number) => any;\n onEvent: (trackIndex: number, event: Event) => any;\n onStart: (trackIndex: number) => any;\n onEnd: (trackIndex: number) => any;\n\n private static deprecatedWarning1 = false;\n\n setAnimationByName(trackIndex: number, animationName: string, loop: boolean) {\n if (!AnimationState.deprecatedWarning1) {\n AnimationState.deprecatedWarning1 = true;\n console.warn('Spine Deprecation Warning: AnimationState.setAnimationByName is deprecated, please use setAnimation from now on.');\n }\n this.setAnimation(trackIndex, animationName, loop);\n }\n\n private static deprecatedWarning2 = false;\n\n addAnimationByName(trackIndex: number, animationName: string, loop: boolean, delay: number) {\n if (!AnimationState.deprecatedWarning2) {\n AnimationState.deprecatedWarning2 = true;\n console.warn('Spine Deprecation Warning: AnimationState.addAnimationByName is deprecated, please use addAnimation from now on.');\n }\n this.addAnimation(trackIndex, animationName, loop, delay);\n }\n\n private static deprecatedWarning3 = false;\n\n hasAnimation(animationName: string): boolean {\n const animation = this.data.skeletonData.findAnimation(animationName);\n\n return animation !== null;\n }\n\n hasAnimationByName(animationName: string): boolean {\n if (!AnimationState.deprecatedWarning3) {\n AnimationState.deprecatedWarning3 = true;\n console.warn('Spine Deprecation Warning: AnimationState.hasAnimationByName is deprecated, please use hasAnimation from now on.');\n }\n\n return this.hasAnimation(animationName);\n }\n}\n\n/** Stores settings and other state for the playback of an animation on an {@link AnimationState} track.\n *\n * References to a track entry must not be kept after the {@link AnimationStateListener#dispose()} event occurs. */\n/**\n * @public\n */\nexport class TrackEntry implements ITrackEntry {\n /** The animation to apply for this track entry. */\n animation: Animation;\n\n /** The animation queued to start after this animation, or null. `next` makes up a linked list. */\n next: TrackEntry;\n\n /** The track entry for the previous animation when mixing from the previous animation to this animation, or null if no\n * mixing is currently occuring. When mixing from multiple animations, `mixingFrom` makes up a linked list. */\n mixingFrom: TrackEntry;\n\n /** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is\n * currently occuring. When mixing to multiple animations, `mixingTo` makes up a linked list. */\n mixingTo: TrackEntry;\n\n /** The listener for events generated by this track entry, or null.\n *\n * A track entry returned from {@link AnimationState#setAnimation()} is already the current animation\n * for the track, so the track entry listener {@link AnimationStateListener#start()} will not be called. */\n listener: AnimationStateListener;\n\n /** The index of the track where this track entry is either current or queued.\n *\n * See {@link AnimationState#getCurrent()}. */\n trackIndex: number;\n\n /** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its\n * duration. */\n loop: boolean;\n\n /** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead\n * of being mixed out.\n *\n * When mixing between animations that key the same property, if a lower track also keys that property then the value will\n * briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%\n * while the second animation mixes from 0% to 100%. Setting `holdPrevious` to true applies the first animation\n * at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which\n * keys the property, only when a higher track also keys the property.\n *\n * Snapping will occur if `holdPrevious` is true and this animation does not key all the same properties as the\n * previous animation. */\n holdPrevious: boolean;\n\n /** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the\n * `eventThreshold`, event timelines are applied while this animation is being mixed out. Defaults to 0, so event\n * timelines are not applied while this animation is being mixed out. */\n eventThreshold: number;\n\n /** When the mix percentage ({@link #mixtime} / {@link #mixDuration}) is less than the\n * `attachmentThreshold`, attachment timelines are applied while this animation is being mixed out. Defaults to\n * 0, so attachment timelines are not applied while this animation is being mixed out. */\n attachmentThreshold: number;\n\n /** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the\n * `drawOrderThreshold`, draw order timelines are applied while this animation is being mixed out. Defaults to 0,\n * so draw order timelines are not applied while this animation is being mixed out. */\n drawOrderThreshold: number;\n\n /** Seconds when this animation starts, both initially and after looping. Defaults to 0.\n *\n * When changing the `animationStart` time, it often makes sense to set {@link #animationLast} to the same\n * value to prevent timeline keys before the start time from triggering. */\n animationStart: number;\n\n /** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will\n * loop back to {@link #animationStart} at this time. Defaults to the animation {@link Animation#duration}. */\n animationEnd: number;\n\n /** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, when this\n * animation is applied, event timelines will fire all events between the `animationLast` time (exclusive) and\n * `animationTime` (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this animation\n * is applied. */\n animationLast: number;\n\n nextAnimationLast: number;\n\n /** Seconds to postpone playing the animation. When this track entry is the current track entry, `delay`\n * postpones incrementing the {@link #trackTime}. When this track entry is queued, `delay` is the time from\n * the start of the previous animation to when this track entry will become the current track entry (ie when the previous\n * track entry {@link TrackEntry#trackTime} >= this track entry's `delay`).\n *\n * {@link #timeScale} affects the delay. */\n delay: number;\n\n /** Current time in seconds this track entry has been the current track entry. The track time determines\n * {@link #animationTime}. The track time can be set to start the animation at a time other than 0, without affecting\n * looping. */\n trackTime: number;\n\n trackLast: number;\n nextTrackLast: number;\n\n /** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float\n * value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time\n * is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the\n * properties keyed by the animation are set to the setup pose and the track is cleared.\n *\n * It may be desired to use {@link AnimationState#addEmptyAnimation()} rather than have the animation\n * abruptly cease being applied. */\n trackEnd: number;\n\n /** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or\n * faster. Defaults to 1.\n *\n * {@link #mixTime} is not affected by track entry time scale, so {@link #mixDuration} may need to be adjusted to\n * match the animation speed.\n *\n * When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the\n * {@link #delay} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If\n * the time scale is not 1, the delay may need to be adjusted.\n *\n * See AnimationState {@link AnimationState#timeScale} for affecting all animations. */\n timeScale: number;\n\n /** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults\n * to 1, which overwrites the skeleton's current pose with this animation.\n *\n * Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to\n * use alpha on track 0 if the skeleton pose is from the last frame render. */\n alpha: number;\n\n /** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be\n * slightly more than `mixDuration` when the mix is complete. */\n mixTime: number;\n\n /** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by AnimationStateData\n * {@link AnimationStateData#getMix()} based on the animation before this animation (if any).\n *\n * A mix duration of 0 still mixes out over one frame to provide the track entry being mixed out a chance to revert the\n * properties it was animating.\n *\n * The `mixDuration` can be set manually rather than use the value from\n * {@link AnimationStateData#getMix()}. In that case, the `mixDuration` can be set for a new\n * track entry only before {@link AnimationState#update(float)} is first called.\n *\n * When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the\n * {@link #delay} is set using the mix duration from the {@link AnimationStateData}, not a mix duration set\n * afterward. */\n mixDuration: number;\n interruptAlpha: number;\n totalAlpha: number;\n\n /** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which\n * replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to\n * the values from the lower tracks.\n *\n * The `mixBlend` can be set for a new track entry only before {@link AnimationState#apply()} is first\n * called. */\n mixBlend = MixBlend.replace;\n timelineMode = new Array<number>();\n timelineHoldMix = new Array<TrackEntry>();\n timelinesRotation =