@pixi-spine/runtime-3.8
Version:
Pixi runtime for spine 3.8 models
992 lines (988 loc) • 41.6 kB
JavaScript
'use strict';
var base = require('@pixi-spine/base');
var Animation = require('./Animation.js');
const _AnimationState = class {
constructor(data) {
/** The list of tracks that currently have animations, which may contain null entries. */
this.tracks = new Array();
/** Multiplier for the delta time when the animation state is updated, causing time for all animations and mixes to play slower
* or faster. Defaults to 1.
*
* See TrackEntry {@link TrackEntry#timeScale} for affecting a single animation. */
this.timeScale = 1;
this.unkeyedState = 0;
this.events = new Array();
this.listeners = new Array();
this.queue = new EventQueue(this);
this.propertyIDs = new base.IntSet();
this.animationsChanged = false;
this.trackEntryPool = new base.Pool(() => new TrackEntry());
this.data = data;
}
/** Increments each track entry {@link TrackEntry#trackTime()}, setting queued animations as current if needed. */
update(delta) {
delta *= this.timeScale;
const tracks = this.tracks;
for (let i = 0, n = tracks.length; i < n; i++) {
const current = tracks[i];
if (current == null)
continue;
current.animationLast = current.nextAnimationLast;
current.trackLast = current.nextTrackLast;
let currentDelta = delta * current.timeScale;
if (current.delay > 0) {
current.delay -= currentDelta;
if (current.delay > 0)
continue;
currentDelta = -current.delay;
current.delay = 0;
}
let next = current.next;
if (next != null) {
const nextTime = current.trackLast - next.delay;
if (nextTime >= 0) {
next.delay = 0;
next.trackTime += current.timeScale == 0 ? 0 : (nextTime / current.timeScale + delta) * next.timeScale;
current.trackTime += currentDelta;
this.setCurrent(i, next, true);
while (next.mixingFrom != null) {
next.mixTime += delta;
next = next.mixingFrom;
}
continue;
}
} else if (current.trackLast >= current.trackEnd && current.mixingFrom == null) {
tracks[i] = null;
this.queue.end(current);
this.disposeNext(current);
continue;
}
if (current.mixingFrom != null && this.updateMixingFrom(current, delta)) {
let from = current.mixingFrom;
current.mixingFrom = null;
if (from != null)
from.mixingTo = null;
while (from != null) {
this.queue.end(from);
from = from.mixingFrom;
}
}
current.trackTime += currentDelta;
}
this.queue.drain();
}
/** Returns true when all mixing from entries are complete. */
updateMixingFrom(to, delta) {
const from = to.mixingFrom;
if (from == null)
return true;
const finished = this.updateMixingFrom(from, delta);
from.animationLast = from.nextAnimationLast;
from.trackLast = from.nextTrackLast;
if (to.mixTime > 0 && to.mixTime >= to.mixDuration) {
if (from.totalAlpha == 0 || to.mixDuration == 0) {
to.mixingFrom = from.mixingFrom;
if (from.mixingFrom != null)
from.mixingFrom.mixingTo = to;
to.interruptAlpha = from.interruptAlpha;
this.queue.end(from);
}
return finished;
}
from.trackTime += delta * from.timeScale;
to.mixTime += delta;
return false;
}
/** Poses the skeleton using the track entry animations. There are no side effects other than invoking listeners, so the
* animation state can be applied to multiple skeletons to pose them identically.
* @returns True if any animations were applied. */
apply(skeleton) {
if (skeleton == null)
throw new Error("skeleton cannot be null.");
if (this.animationsChanged)
this._animationsChanged();
const events = this.events;
const tracks = this.tracks;
let applied = false;
for (let i = 0, n = tracks.length; i < n; i++) {
const current = tracks[i];
if (current == null || current.delay > 0)
continue;
applied = true;
const blend = i == 0 ? base.MixBlend.first : current.mixBlend;
let mix = current.alpha;
if (current.mixingFrom != null)
mix *= this.applyMixingFrom(current, skeleton, blend);
else if (current.trackTime >= current.trackEnd && current.next == null)
mix = 0;
const animationLast = current.animationLast;
const animationTime = current.getAnimationTime();
const timelineCount = current.animation.timelines.length;
const timelines = current.animation.timelines;
if (i == 0 && mix == 1 || blend == base.MixBlend.add) {
for (let ii = 0; ii < timelineCount; ii++) {
base.Utils.webkit602BugfixHelper(mix, blend);
const timeline = timelines[ii];
if (timeline instanceof Animation.AttachmentTimeline)
this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true);
else
timeline.apply(skeleton, animationLast, animationTime, events, mix, blend, base.MixDirection.mixIn);
}
} else {
const timelineMode = current.timelineMode;
const firstFrame = current.timelinesRotation.length == 0;
if (firstFrame)
base.Utils.setArraySize(current.timelinesRotation, timelineCount << 1, null);
const timelinesRotation = current.timelinesRotation;
for (let ii = 0; ii < timelineCount; ii++) {
const timeline = timelines[ii];
const timelineBlend = timelineMode[ii] == _AnimationState.SUBSEQUENT ? blend : base.MixBlend.setup;
if (timeline instanceof Animation.RotateTimeline) {
this.applyRotateTimeline(timeline, skeleton, animationTime, mix, timelineBlend, timelinesRotation, ii << 1, firstFrame);
} else if (timeline instanceof Animation.AttachmentTimeline) {
this.applyAttachmentTimeline(timeline, skeleton, animationTime, blend, true);
} else {
base.Utils.webkit602BugfixHelper(mix, blend);
timeline.apply(skeleton, animationLast, animationTime, events, mix, timelineBlend, base.MixDirection.mixIn);
}
}
}
this.queueEvents(current, animationTime);
events.length = 0;
current.nextAnimationLast = animationTime;
current.nextTrackLast = current.trackTime;
}
const setupState = this.unkeyedState + _AnimationState.SETUP;
const slots = skeleton.slots;
for (let i = 0, n = skeleton.slots.length; i < n; i++) {
const slot = slots[i];
if (slot.attachmentState == setupState) {
const attachmentName = slot.data.attachmentName;
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
}
}
this.unkeyedState += 2;
this.queue.drain();
return applied;
}
applyMixingFrom(to, skeleton, blend) {
const from = to.mixingFrom;
if (from.mixingFrom != null)
this.applyMixingFrom(from, skeleton, blend);
let mix = 0;
if (to.mixDuration == 0) {
mix = 1;
if (blend == base.MixBlend.first)
blend = base.MixBlend.setup;
} else {
mix = to.mixTime / to.mixDuration;
if (mix > 1)
mix = 1;
if (blend != base.MixBlend.first)
blend = from.mixBlend;
}
const events = mix < from.eventThreshold ? this.events : null;
const attachments = mix < from.attachmentThreshold;
const drawOrder = mix < from.drawOrderThreshold;
const animationLast = from.animationLast;
const animationTime = from.getAnimationTime();
const timelineCount = from.animation.timelines.length;
const timelines = from.animation.timelines;
const alphaHold = from.alpha * to.interruptAlpha;
const alphaMix = alphaHold * (1 - mix);
if (blend == base.MixBlend.add) {
for (let i = 0; i < timelineCount; i++)
timelines[i].apply(skeleton, animationLast, animationTime, events, alphaMix, blend, base.MixDirection.mixOut);
} else {
const timelineMode = from.timelineMode;
const timelineHoldMix = from.timelineHoldMix;
const firstFrame = from.timelinesRotation.length == 0;
if (firstFrame)
base.Utils.setArraySize(from.timelinesRotation, timelineCount << 1, null);
const timelinesRotation = from.timelinesRotation;
from.totalAlpha = 0;
for (let i = 0; i < timelineCount; i++) {
const timeline = timelines[i];
let direction = base.MixDirection.mixOut;
let timelineBlend;
let alpha = 0;
switch (timelineMode[i]) {
case _AnimationState.SUBSEQUENT:
if (!drawOrder && timeline instanceof Animation.DrawOrderTimeline)
continue;
timelineBlend = blend;
alpha = alphaMix;
break;
case _AnimationState.FIRST:
timelineBlend = base.MixBlend.setup;
alpha = alphaMix;
break;
case _AnimationState.HOLD_SUBSEQUENT:
timelineBlend = blend;
alpha = alphaHold;
break;
case _AnimationState.HOLD_FIRST:
timelineBlend = base.MixBlend.setup;
alpha = alphaHold;
break;
default:
timelineBlend = base.MixBlend.setup;
const holdMix = timelineHoldMix[i];
alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
break;
}
from.totalAlpha += alpha;
if (timeline instanceof Animation.RotateTimeline)
this.applyRotateTimeline(timeline, skeleton, animationTime, alpha, timelineBlend, timelinesRotation, i << 1, firstFrame);
else if (timeline instanceof Animation.AttachmentTimeline)
this.applyAttachmentTimeline(timeline, skeleton, animationTime, timelineBlend, attachments);
else {
base.Utils.webkit602BugfixHelper(alpha, blend);
if (drawOrder && timeline instanceof Animation.DrawOrderTimeline && timelineBlend == base.MixBlend.setup)
direction = base.MixDirection.mixIn;
timeline.apply(skeleton, animationLast, animationTime, events, alpha, timelineBlend, direction);
}
}
}
if (to.mixDuration > 0)
this.queueEvents(from, animationTime);
this.events.length = 0;
from.nextAnimationLast = animationTime;
from.nextTrackLast = from.trackTime;
return mix;
}
applyAttachmentTimeline(timeline, skeleton, time, blend, attachments) {
const slot = skeleton.slots[timeline.slotIndex];
if (!slot.bone.active)
return;
const frames = timeline.frames;
if (time < frames[0]) {
if (blend == base.MixBlend.setup || blend == base.MixBlend.first)
this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else {
let frameIndex;
if (time >= frames[frames.length - 1])
frameIndex = frames.length - 1;
else
frameIndex = Animation.Animation.binarySearch(frames, time) - 1;
this.setAttachment(skeleton, slot, timeline.attachmentNames[frameIndex], attachments);
}
if (slot.attachmentState <= this.unkeyedState)
slot.attachmentState = this.unkeyedState + _AnimationState.SETUP;
}
setAttachment(skeleton, slot, attachmentName, attachments) {
slot.setAttachment(attachmentName == null ? null : skeleton.getAttachment(slot.data.index, attachmentName));
if (attachments)
slot.attachmentState = this.unkeyedState + _AnimationState.CURRENT;
}
applyRotateTimeline(timeline, skeleton, time, alpha, blend, timelinesRotation, i, firstFrame) {
if (firstFrame)
timelinesRotation[i] = 0;
if (alpha == 1) {
timeline.apply(skeleton, 0, time, null, 1, blend, base.MixDirection.mixIn);
return;
}
const rotateTimeline = timeline;
const frames = rotateTimeline.frames;
const bone = skeleton.bones[rotateTimeline.boneIndex];
if (!bone.active)
return;
let r1 = 0;
let r2 = 0;
if (time < frames[0]) {
switch (blend) {
case base.MixBlend.setup:
bone.rotation = bone.data.rotation;
default:
return;
case base.MixBlend.first:
r1 = bone.rotation;
r2 = bone.data.rotation;
}
} else {
r1 = blend == base.MixBlend.setup ? bone.data.rotation : bone.rotation;
if (time >= frames[frames.length - Animation.RotateTimeline.ENTRIES])
r2 = bone.data.rotation + frames[frames.length + Animation.RotateTimeline.PREV_ROTATION];
else {
const frame = Animation.Animation.binarySearch(frames, time, Animation.RotateTimeline.ENTRIES);
const prevRotation = frames[frame + Animation.RotateTimeline.PREV_ROTATION];
const frameTime = frames[frame];
const percent = rotateTimeline.getCurvePercent((frame >> 1) - 1, 1 - (time - frameTime) / (frames[frame + Animation.RotateTimeline.PREV_TIME] - frameTime));
r2 = frames[frame + Animation.RotateTimeline.ROTATION] - prevRotation;
r2 -= (16384 - (16384.499999999996 - r2 / 360 | 0)) * 360;
r2 = prevRotation + r2 * percent + bone.data.rotation;
r2 -= (16384 - (16384.499999999996 - r2 / 360 | 0)) * 360;
}
}
let total = 0;
let diff = r2 - r1;
diff -= (16384 - (16384.499999999996 - diff / 360 | 0)) * 360;
if (diff == 0) {
total = timelinesRotation[i];
} else {
let lastTotal = 0;
let lastDiff = 0;
if (firstFrame) {
lastTotal = 0;
lastDiff = diff;
} else {
lastTotal = timelinesRotation[i];
lastDiff = timelinesRotation[i + 1];
}
const current = diff > 0;
let dir = lastTotal >= 0;
if (base.MathUtils.signum(lastDiff) != base.MathUtils.signum(diff) && Math.abs(lastDiff) <= 90) {
if (Math.abs(lastTotal) > 180)
lastTotal += 360 * base.MathUtils.signum(lastTotal);
dir = current;
}
total = diff + lastTotal - lastTotal % 360;
if (dir != current)
total += 360 * base.MathUtils.signum(lastTotal);
timelinesRotation[i] = total;
}
timelinesRotation[i + 1] = diff;
r1 += total * alpha;
bone.rotation = r1 - (16384 - (16384.499999999996 - r1 / 360 | 0)) * 360;
}
queueEvents(entry, animationTime) {
const animationStart = entry.animationStart;
const animationEnd = entry.animationEnd;
const duration = animationEnd - animationStart;
const trackLastWrapped = entry.trackLast % duration;
const events = this.events;
let i = 0;
const n = events.length;
for (; i < n; i++) {
const event = events[i];
if (event.time < trackLastWrapped)
break;
if (event.time > animationEnd)
continue;
this.queue.event(entry, event);
}
let complete = false;
if (entry.loop)
complete = duration == 0 || trackLastWrapped > entry.trackTime % duration;
else
complete = animationTime >= animationEnd && entry.animationLast < animationEnd;
if (complete)
this.queue.complete(entry);
for (; i < n; i++) {
const event = events[i];
if (event.time < animationStart)
continue;
this.queue.event(entry, events[i]);
}
}
/** Removes all animations from all tracks, leaving skeletons in their current pose.
*
* It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose,
* rather than leaving them in their current pose. */
clearTracks() {
const oldDrainDisabled = this.queue.drainDisabled;
this.queue.drainDisabled = true;
for (let i = 0, n = this.tracks.length; i < n; i++)
this.clearTrack(i);
this.tracks.length = 0;
this.queue.drainDisabled = oldDrainDisabled;
this.queue.drain();
}
/** Removes all animations from the track, leaving skeletons in their current pose.
*
* It may be desired to use {@link AnimationState#setEmptyAnimation()} to mix the skeletons back to the setup pose,
* rather than leaving them in their current pose. */
clearTrack(trackIndex) {
if (trackIndex >= this.tracks.length)
return;
const current = this.tracks[trackIndex];
if (current == null)
return;
this.queue.end(current);
this.disposeNext(current);
let entry = current;
while (true) {
const from = entry.mixingFrom;
if (from == null)
break;
this.queue.end(from);
entry.mixingFrom = null;
entry.mixingTo = null;
entry = from;
}
this.tracks[current.trackIndex] = null;
this.queue.drain();
}
setCurrent(index, current, interrupt) {
const from = this.expandToIndex(index);
this.tracks[index] = current;
if (from != null) {
if (interrupt)
this.queue.interrupt(from);
current.mixingFrom = from;
from.mixingTo = current;
current.mixTime = 0;
if (from.mixingFrom != null && from.mixDuration > 0)
current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration);
from.timelinesRotation.length = 0;
}
this.queue.start(current);
}
/** Sets an animation by name.
*
* {@link #setAnimationWith(}. */
setAnimation(trackIndex, animationName, loop) {
const animation = this.data.skeletonData.findAnimation(animationName);
if (animation == null)
throw new Error(`Animation not found: ${animationName}`);
return this.setAnimationWith(trackIndex, animation, loop);
}
/** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never
* applied to a skeleton, it is replaced (not mixed from).
* @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
* duration. In either case {@link TrackEntry#trackEnd} determines when the track is cleared.
* @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept
* after the {@link AnimationStateListener#dispose()} event occurs. */
setAnimationWith(trackIndex, animation, loop) {
if (animation == null)
throw new Error("animation cannot be null.");
let interrupt = true;
let current = this.expandToIndex(trackIndex);
if (current != null) {
if (current.nextTrackLast == -1) {
this.tracks[trackIndex] = current.mixingFrom;
this.queue.interrupt(current);
this.queue.end(current);
this.disposeNext(current);
current = current.mixingFrom;
interrupt = false;
} else
this.disposeNext(current);
}
const entry = this.trackEntry(trackIndex, animation, loop, current);
this.setCurrent(trackIndex, entry, interrupt);
this.queue.drain();
return entry;
}
/** Queues an animation by name.
*
* See {@link #addAnimationWith()}. */
addAnimation(trackIndex, animationName, loop, delay) {
const animation = this.data.skeletonData.findAnimation(animationName);
if (animation == null)
throw new Error(`Animation not found: ${animationName}`);
return this.addAnimationWith(trackIndex, animation, loop, delay);
}
/** Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is
* equivalent to calling {@link #setAnimationWith()}.
* @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry
* minus any mix duration (from the {@link AnimationStateData}) plus the specified `delay` (ie the mix
* ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the
* previous entry is looping, its next loop completion is used instead of its duration.
* @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept
* after the {@link AnimationStateListener#dispose()} event occurs. */
addAnimationWith(trackIndex, animation, loop, delay) {
if (animation == null)
throw new Error("animation cannot be null.");
let last = this.expandToIndex(trackIndex);
if (last != null) {
while (last.next != null)
last = last.next;
}
const entry = this.trackEntry(trackIndex, animation, loop, last);
if (last == null) {
this.setCurrent(trackIndex, entry, true);
this.queue.drain();
} else {
last.next = entry;
if (delay <= 0) {
const duration = last.animationEnd - last.animationStart;
if (duration != 0) {
if (last.loop)
delay += duration * (1 + (last.trackTime / duration | 0));
else
delay += Math.max(duration, last.trackTime);
delay -= this.data.getMix(last.animation, animation);
} else
delay = last.trackTime;
}
}
entry.delay = delay;
return entry;
}
/** Sets an empty animation for a track, discarding any queued animations, and sets the track entry's
* {@link TrackEntry#mixduration}. An empty animation has no timelines and serves as a placeholder for mixing in or out.
*
* Mixing out is done by setting an empty animation with a mix duration using either {@link #setEmptyAnimation()},
* {@link #setEmptyAnimations()}, or {@link #addEmptyAnimation()}. Mixing to an empty animation causes
* the previous animation to be applied less and less over the mix duration. Properties keyed in the previous animation
* transition to the value from lower tracks or to the setup pose value if no lower tracks key the property. A mix duration of
* 0 still mixes out over one frame.
*
* Mixing in is done by first setting an empty animation, then adding an animation using
* {@link #addAnimation()} and on the returned track entry, set the
* {@link TrackEntry#setMixDuration()}. Mixing from an empty animation causes the new animation to be applied more and
* more over the mix duration. Properties keyed in the new animation transition from the value from lower tracks or from the
* setup pose value if no lower tracks key the property to the value keyed in the new animation. */
setEmptyAnimation(trackIndex, mixDuration) {
const entry = this.setAnimationWith(trackIndex, _AnimationState.emptyAnimation, false);
entry.mixDuration = mixDuration;
entry.trackEnd = mixDuration;
return entry;
}
/** Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
* {@link TrackEntry#mixDuration}. If the track is empty, it is equivalent to calling
* {@link #setEmptyAnimation()}.
*
* See {@link #setEmptyAnimation()}.
* @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry
* minus any mix duration plus the specified `delay` (ie the mix ends at (`delay` = 0) or
* before (`delay` < 0) the previous track entry duration). If the previous entry is looping, its next
* loop completion is used instead of its duration.
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
* after the {@link AnimationStateListener#dispose()} event occurs. */
addEmptyAnimation(trackIndex, mixDuration, delay) {
if (delay <= 0)
delay -= mixDuration;
const entry = this.addAnimationWith(trackIndex, _AnimationState.emptyAnimation, false, delay);
entry.mixDuration = mixDuration;
entry.trackEnd = mixDuration;
return entry;
}
/** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix
* duration. */
setEmptyAnimations(mixDuration) {
const oldDrainDisabled = this.queue.drainDisabled;
this.queue.drainDisabled = true;
for (let i = 0, n = this.tracks.length; i < n; i++) {
const current = this.tracks[i];
if (current != null)
this.setEmptyAnimation(current.trackIndex, mixDuration);
}
this.queue.drainDisabled = oldDrainDisabled;
this.queue.drain();
}
expandToIndex(index) {
if (index < this.tracks.length)
return this.tracks[index];
base.Utils.ensureArrayCapacity(this.tracks, index + 1, null);
this.tracks.length = index + 1;
return null;
}
/** @param last May be null. */
trackEntry(trackIndex, animation, loop, last) {
const entry = this.trackEntryPool.obtain();
entry.trackIndex = trackIndex;
entry.animation = animation;
entry.loop = loop;
entry.holdPrevious = false;
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;
entry.trackEnd = Number.MAX_VALUE;
entry.timeScale = 1;
entry.alpha = 1;
entry.interruptAlpha = 1;
entry.mixTime = 0;
entry.mixDuration = last == null ? 0 : this.data.getMix(last.animation, animation);
entry.mixBlend = base.MixBlend.replace;
return entry;
}
disposeNext(entry) {
let next = entry.next;
while (next != null) {
this.queue.dispose(next);
next = next.next;
}
entry.next = null;
}
_animationsChanged() {
this.animationsChanged = false;
this.propertyIDs.clear();
for (let i = 0, n = this.tracks.length; i < n; i++) {
let entry = this.tracks[i];
if (entry == null)
continue;
while (entry.mixingFrom != null)
entry = entry.mixingFrom;
do {
if (entry.mixingFrom == null || entry.mixBlend != base.MixBlend.add)
this.computeHold(entry);
entry = entry.mixingTo;
} while (entry != null);
}
}
computeHold(entry) {
const to = entry.mixingTo;
const timelines = entry.animation.timelines;
const timelinesCount = entry.animation.timelines.length;
const timelineMode = base.Utils.setArraySize(entry.timelineMode, timelinesCount);
entry.timelineHoldMix.length = 0;
const timelineDipMix = base.Utils.setArraySize(entry.timelineHoldMix, timelinesCount);
const propertyIDs = this.propertyIDs;
if (to != null && to.holdPrevious) {
for (let i = 0; i < timelinesCount; i++) {
timelineMode[i] = propertyIDs.add(timelines[i].getPropertyId()) ? _AnimationState.HOLD_FIRST : _AnimationState.HOLD_SUBSEQUENT;
}
return;
}
outer:
for (let i = 0; i < timelinesCount; i++) {
const timeline = timelines[i];
const id = timeline.getPropertyId();
if (!propertyIDs.add(id))
timelineMode[i] = _AnimationState.SUBSEQUENT;
else if (to == null || timeline instanceof Animation.AttachmentTimeline || timeline instanceof Animation.DrawOrderTimeline || timeline instanceof Animation.EventTimeline || !to.animation.hasTimeline(id)) {
timelineMode[i] = _AnimationState.FIRST;
} else {
for (let next = to.mixingTo; next != null; next = next.mixingTo) {
if (next.animation.hasTimeline(id))
continue;
if (entry.mixDuration > 0) {
timelineMode[i] = _AnimationState.HOLD_MIX;
timelineDipMix[i] = next;
continue outer;
}
break;
}
timelineMode[i] = _AnimationState.HOLD_FIRST;
}
}
}
/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
getCurrent(trackIndex) {
if (trackIndex >= this.tracks.length)
return null;
return this.tracks[trackIndex];
}
/** Adds a listener to receive events for all track entries. */
addListener(listener) {
if (listener == null)
throw new Error("listener cannot be null.");
this.listeners.push(listener);
}
/** Removes the listener added with {@link #addListener()}. */
removeListener(listener) {
const index = this.listeners.indexOf(listener);
if (index >= 0)
this.listeners.splice(index, 1);
}
/** Removes all listeners added with {@link #addListener()}. */
clearListeners() {
this.listeners.length = 0;
}
/** Discards all listener notifications that have not yet been delivered. This can be useful to call from an
* {@link AnimationStateListener} when it is known that further notifications that may have been already queued for delivery
* are not wanted because new animations are being set. */
clearListenerNotifications() {
this.queue.clear();
}
setAnimationByName(trackIndex, animationName, loop) {
if (!_AnimationState.deprecatedWarning1) {
_AnimationState.deprecatedWarning1 = true;
console.warn("Spine Deprecation Warning: AnimationState.setAnimationByName is deprecated, please use setAnimation from now on.");
}
this.setAnimation(trackIndex, animationName, loop);
}
addAnimationByName(trackIndex, animationName, loop, delay) {
if (!_AnimationState.deprecatedWarning2) {
_AnimationState.deprecatedWarning2 = true;
console.warn("Spine Deprecation Warning: AnimationState.addAnimationByName is deprecated, please use addAnimation from now on.");
}
this.addAnimation(trackIndex, animationName, loop, delay);
}
hasAnimation(animationName) {
const animation = this.data.skeletonData.findAnimation(animationName);
return animation !== null;
}
hasAnimationByName(animationName) {
if (!_AnimationState.deprecatedWarning3) {
_AnimationState.deprecatedWarning3 = true;
console.warn("Spine Deprecation Warning: AnimationState.hasAnimationByName is deprecated, please use hasAnimation from now on.");
}
return this.hasAnimation(animationName);
}
};
let AnimationState = _AnimationState;
AnimationState.emptyAnimation = new Animation.Animation("<empty>", [], 0);
/** 1. A previously applied timeline has set this property.
*
* Result: Mix from the current pose to the timeline pose. */
AnimationState.SUBSEQUENT = 0;
/** 1. This is the first timeline to set this property.
* 2. The next track entry applied after this one does not have a timeline to set this property.
*
* Result: Mix from the setup pose to the timeline pose. */
AnimationState.FIRST = 1;
/** 1) A previously applied timeline has set this property.<br>
* 2) The next track entry to be applied does have a timeline to set this property.<br>
* 3) The next track entry after that one does not have a timeline to set this property.<br>
* Result: Mix from the current pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading
* animations that key the same property. A subsequent timeline will set this property using a mix. */
AnimationState.HOLD_SUBSEQUENT = 2;
/** 1) This is the first timeline to set this property.<br>
* 2) The next track entry to be applied does have a timeline to set this property.<br>
* 3) The next track entry after that one does not have a timeline to set this property.<br>
* Result: Mix from the setup pose to the timeline pose, but do not mix out. This avoids "dipping" when crossfading animations
* that key the same property. A subsequent timeline will set this property using a mix. */
AnimationState.HOLD_FIRST = 3;
/** 1. This is the first timeline to set this property.
* 2. The next track entry to be applied does have a timeline to set this property.
* 3. The next track entry after that one does have a timeline to set this property.
* 4. timelineHoldMix stores the first subsequent track entry that does not have a timeline to set this property.
*
* Result: The same as HOLD except the mix percentage from the timelineHoldMix track entry is used. This handles when more than
* 2 track entries in a row have a timeline that sets the same property.
*
* 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
* "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
* (which affects B and C). Without using D to mix out, A would be applied fully until mixing completes, then snap into
* place. */
AnimationState.HOLD_MIX = 4;
AnimationState.SETUP = 1;
AnimationState.CURRENT = 2;
AnimationState.deprecatedWarning1 = false;
AnimationState.deprecatedWarning2 = false;
AnimationState.deprecatedWarning3 = false;
const _TrackEntry = class {
constructor() {
/** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which
* replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to
* the values from the lower tracks.
*
* The `mixBlend` can be set for a new track entry only before {@link AnimationState#apply()} is first
* called. */
this.mixBlend = base.MixBlend.replace;
this.timelineMode = new Array();
this.timelineHoldMix = new Array();
this.timelinesRotation = new Array();
}
reset() {
this.next = null;
this.mixingFrom = null;
this.mixingTo = null;
this.animation = null;
this.listener = null;
this.timelineMode.length = 0;
this.timelineHoldMix.length = 0;
this.timelinesRotation.length = 0;
}
/** Uses {@link #trackTime} to compute the `animationTime`, which is between {@link #animationStart}
* and {@link #animationEnd}. When the `trackTime` is 0, the `animationTime` is equal to the
* `animationStart` time. */
getAnimationTime() {
if (this.loop) {
const duration = this.animationEnd - this.animationStart;
if (duration == 0)
return this.animationStart;
return this.trackTime % duration + this.animationStart;
}
return Math.min(this.trackTime + this.animationStart, this.animationEnd);
}
setAnimationLast(animationLast) {
this.animationLast = animationLast;
this.nextAnimationLast = animationLast;
}
/** Returns true if at least one loop has been completed.
*
* See {@link AnimationStateListener#complete()}. */
isComplete() {
return this.trackTime >= this.animationEnd - this.animationStart;
}
/** Resets the rotation directions for mixing this entry's rotate timelines. This can be useful to avoid bones rotating the
* long way around when using {@link #alpha} and starting animations on other tracks.
*
* Mixing with {@link MixBlend#replace} involves finding a rotation between two others, which has two possible solutions:
* the short way or the long way around. The two rotations likely change over time, so which direction is the short or long
* way also changes. If the short way was always chosen, bones would flip to the other side when that direction became the
* long way. TrackEntry chooses the short way the first time it is applied and remembers that direction. */
resetRotationDirections() {
this.timelinesRotation.length = 0;
}
get time() {
if (!_TrackEntry.deprecatedWarning1) {
_TrackEntry.deprecatedWarning1 = true;
console.warn("Spine Deprecation Warning: TrackEntry.time is deprecated, please use trackTime from now on.");
}
return this.trackTime;
}
set time(value) {
if (!_TrackEntry.deprecatedWarning1) {
_TrackEntry.deprecatedWarning1 = true;
console.warn("Spine Deprecation Warning: TrackEntry.time is deprecated, please use trackTime from now on.");
}
this.trackTime = value;
}
get endTime() {
if (!_TrackEntry.deprecatedWarning2) {
_TrackEntry.deprecatedWarning2 = true;
console.warn("Spine Deprecation Warning: TrackEntry.endTime is deprecated, please use trackEnd from now on.");
}
return this.trackTime;
}
set endTime(value) {
if (!_TrackEntry.deprecatedWarning2) {
_TrackEntry.deprecatedWarning2 = true;
console.warn("Spine Deprecation Warning: TrackEntry.endTime is deprecated, please use trackEnd from now on.");
}
this.trackTime = value;
}
loopsCount() {
return Math.floor(this.trackTime / this.trackEnd);
}
};
let TrackEntry = _TrackEntry;
TrackEntry.deprecatedWarning1 = false;
TrackEntry.deprecatedWarning2 = false;
const _EventQueue = class {
constructor(animState) {
this.objects = [];
this.drainDisabled = false;
this.animState = animState;
}
start(entry) {
this.objects.push(EventType.start);
this.objects.push(entry);
this.animState.animationsChanged = true;
}
interrupt(entry) {
this.objects.push(EventType.interrupt);
this.objects.push(entry);
}
end(entry) {
this.objects.push(EventType.end);
this.objects.push(entry);
this.animState.animationsChanged = true;
}
dispose(entry) {
this.objects.push(EventType.dispose);
this.objects.push(entry);
}
complete(entry) {
this.objects.push(EventType.complete);
this.objects.push(entry);
}
event(entry, event) {
this.objects.push(EventType.event);
this.objects.push(entry);
this.objects.push(event);
}
deprecateStuff() {
if (!_EventQueue.deprecatedWarning1) {
_EventQueue.deprecatedWarning1 = true;
console.warn(
"Spine Deprecation Warning: onComplete, onStart, onEnd, onEvent art deprecated, please use listeners from now on. 'state.addListener({ complete: function(track, event) { } })'"
);
}
return true;
}
drain() {
if (this.drainDisabled)
return;
this.drainDisabled = true;
const objects = this.objects;
const listeners = this.animState.listeners;
for (let i = 0; i < objects.length; i += 2) {
const type = objects[i];
const entry = objects[i + 1];
switch (type) {
case EventType.start:
if (entry.listener != null && entry.listener.start)
entry.listener.start(entry);
for (let ii = 0; ii < listeners.length; ii++)
if (listeners[ii].start)
listeners[ii].start(entry);
entry.onStart && this.deprecateStuff() && entry.onStart(entry.trackIndex);
this.animState.onStart && this.deprecateStuff() && this.deprecateStuff && this.animState.onStart(entry.trackIndex);
break;
case EventType.interrupt:
if (entry.listener != null && entry.listener.interrupt)
entry.listener.interrupt(entry);
for (let ii = 0; ii < listeners.length; ii++)
if (listeners[ii].interrupt)
listeners[ii].interrupt(entry);
break;
case EventType.end:
if (entry.listener != null && entry.listener.end)
entry.listener.end(entry);
for (let ii = 0; ii < listeners.length; ii++)
if (listeners[ii].end)
listeners[ii].end(entry);
entry.onEnd && this.deprecateStuff() && entry.onEnd(entry.trackIndex);
this.animState.onEnd && this.deprecateStuff() && this.animState.onEnd(entry.trackIndex);
case EventType.dispose:
if (entry.listener != null && entry.listener.dispose)
entry.listener.dispose(entry);
for (let ii = 0; ii < listeners.length; ii++)
if (listeners[ii].dispose)
listeners[ii].dispose(entry);
this.animState.trackEntryPool.free(entry);
break;
case EventType.complete:
if (entry.listener != null && entry.listener.complete)
entry.listener.complete(entry);
for (let ii = 0; ii < listeners.length; ii++)
if (listeners[ii].complete)
listeners[ii].complete(entry);
const count = base.MathUtils.toInt(entry.loopsCount());
entry.onComplete && this.deprecateStuff() && entry.onComplete(entry.trackIndex, count);
this.animState.onComplete && this.deprecateStuff() && this.animState.onComplete(entry.trackIndex, count);
break;
case EventType.event:
const event = objects[i++ + 2];
if (entry.listener != null && entry.listener.event)
entry.listener.event(entry, event);
for (let ii = 0; ii < listeners.length; ii++)
if (listeners[ii].event)
listeners[ii].event(entry, event);
entry.onEvent && this.deprecateStuff() && entry.onEvent(entry.trackIndex, event);
this.animState.onEvent && this.deprecateStuff() && this.animState.onEvent(entry.trackIndex, event);
break;
}
}
this.clear();
this.drainDisabled = false;
}
clear() {
this.objects.length = 0;
}
};
let EventQueue = _EventQueue;
EventQueue.deprecatedWarning1 = false;
var EventType = /* @__PURE__ */ ((EventType2) => {
EventType2[EventType2["start"] = 0] = "start";
EventType2[EventType2["interrupt"] = 1] = "interrupt";
EventType2[EventType2["end"] = 2] = "end";
EventType2[EventType2["dispose"] = 3] = "dispose";
EventType2[EventType2["complete"] = 4] = "complete";
EventType2[EventType2["event"] = 5] = "event";
return EventType2;
})(EventType || {});
class AnimationStateAdapter {
start(entry) {
}
interrupt(entry) {
}
end(entry) {
}
dispose(entry) {
}
complete(entry) {
}
event(entry, event) {
}
}
exports.AnimationState = AnimationState;
exports.AnimationStateAdapter = AnimationStateAdapter;
exports.EventQueue = EventQueue;
exports.EventType = EventType;
exports.TrackEntry = TrackEntry;
//# sourceMappingURL=AnimationState.js.map