@esotericsoftware/spine-core
Version:
The official Spine Runtimes for the web.
457 lines (456 loc) • 29 kB
TypeScript
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) 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
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
/** biome-ignore-all lint/style/noNonNullAssertion: reference runtime expects some nullable to not be null */
import { Animation, AttachmentTimeline, RotateTimeline } from "./Animation.js";
import type { AnimationStateData } from "./AnimationStateData.js";
import type { Event } from "./Event.js";
import type { Skeleton } from "./Skeleton.js";
import { Interpolation, Pool, StringSet } from "./Utils.js";
/** Applies animations over time, queues animations for later playback, mixes (crossfading) between animations, and applies
* multiple animations on top of each other (layering).
*
* See [Applying Animations](http://esotericsoftware.com/spine-applying-animations#AnimationState-API) in the Spine Runtimes Guide. */
export declare class AnimationState {
static readonly emptyAnimation: Animation;
/** The AnimationStateData to look up mix durations. */
data: AnimationStateData;
/** The list of tracks that have had animations. May contain null entries for tracks that currently have no animation. */
readonly tracks: (TrackEntry | null)[];
/** 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 {@link TrackEntry.timeScale} to affect a single animation. */
timeScale: number;
unkeyedState: number;
readonly events: Event[];
readonly listeners: AnimationStateListener[];
queue: EventQueue;
propertyIds: StringSet;
animationsChanged: boolean;
trackEntryPool: Pool<TrackEntry>;
constructor(data: AnimationStateData);
/** Increments each track entry {@link TrackEntry.trackTime}, setting queued animations as current if needed. */
update(delta: number): void;
/** Returns true when all mixing from entries are complete. */
updateMixingFrom(to: TrackEntry, delta: number): boolean;
/** Poses the skeleton using the track entry animations. The animation state is not changed, so can be applied to multiple
* skeletons to pose them identically.
* @returns True if any animations were applied. */
apply(skeleton: Skeleton): boolean;
applyMixingFrom(to: TrackEntry, skeleton: Skeleton): number;
/** Applies the attachment timeline and sets {@link Slot.attachmentState}.
* @param retain True if the attachment remains after apply, false if temporary for deform timelines. */
applyAttachmentTimeline(timeline: AttachmentTimeline, skeleton: Skeleton, time: number, fromSetup: boolean, retain: boolean): void;
/** Applies the rotate timeline, mixing with the current pose while keeping the same rotation direction chosen as the shortest
* the first time the mixing was applied. */
applyRotateTimeline(timeline: RotateTimeline, skeleton: Skeleton, time: number, alpha: number, fromSetup: boolean, timelinesRotation: Array<number>, i: number, firstFrame: boolean): void;
queueEvents(entry: TrackEntry, animationTime: number): void;
private eventsReverse;
/** Removes all animations from all tracks, leaving skeletons in their current pose.
*
* Usually you want to use {@link setEmptyAnimations} to mix the skeletons back to the setup pose, rather than leaving
* them in their current pose. */
clearTracks(): void;
/** Removes all animations from the track, leaving skeletons in their current pose.
*
* Usually you want to use {@link setEmptyAnimation} to mix the skeletons back to the setup pose, rather than
* leaving them in their current pose. */
clearTrack(trackIndex: number): void;
setTrack(index: number, current: TrackEntry, interrupt: boolean): void;
/** Sets an animation by name.
*
* See {@link setAnimation}. */
setAnimation(trackIndex: number, animationName: string, loop?: boolean): TrackEntry;
/** Sets the current animation for a track, discarding any queued animations.
*
* If the formerly current track entry is for the same animation and 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.
* @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. */
setAnimation(trackIndex: number, animation: Animation, loop?: boolean): TrackEntry;
private setAnimation1;
/** Sets the current animation for a track, discarding any queued animations.
*
* If the formerly current track entry is for the same animation and 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.getTrackEnd} determines when the track is cleared.
* @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. */
private setAnimation2;
/** Queues an animation by name.
*
* See {@link addAnimation}. */
addAnimation(trackIndex: number, animationName: string, loop?: boolean, delay?: number): TrackEntry;
/** Adds an animation to be played after the current or last queued animation for a track. If the track has no entries, this is
* equivalent to calling {@link setAnimation}.
* @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 {@link data}) plus the specified `delay` (ie the mix ends at (when
* `delay` = 0) or before (when `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. */
addAnimation(trackIndex: number, animation: Animation, loop?: boolean, delay?: number): TrackEntry;
private addAnimation1;
private addAnimation2;
/** 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 needs to be applied one more time to mix out, so the properties it was animating are reverted.
*
* Mixing in is done by first setting an empty animation, then adding an animation using
* {@link addAnimation} with the desired delay (an empty animation has a duration of 0) 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.
*
* See <a href='https://esotericsoftware.com/spine-applying-animations#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide. */
setEmptyAnimation(trackIndex: number, mixDuration?: number): TrackEntry;
/** 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 has no entries, it is equivalent to calling
* {@link setEmptyAnimation}.
*
* See {@link setEmptyAnimation} and
* <a href='https://esotericsoftware.com/spine-applying-animations#Empty-animations'>Empty animations</a> in the Spine Runtimes
* Guide.
* @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 (when `delay` = 0) or before
* (when `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: number, mixDuration?: number, delay?: number): TrackEntry;
/** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration.
*
* See <a href='https://esotericsoftware.com/spine-applying-animations#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide. */
setEmptyAnimations(mixDuration?: number): void;
expandToIndex(index: number): TrackEntry | null;
/** @param last May be null. */
trackEntry(trackIndex: number, animation: Animation, loop: boolean, last: TrackEntry | null): TrackEntry;
/** Removes {@link TrackEntry.next} and all entries after it for the specified entry. */
clearNext(entry: TrackEntry): void;
_animationsChanged(): void;
computeHold(entry: TrackEntry): void;
/** Returns the track entry for the animation currently playing on the track, or null if no animation is currently playing. */
getTrack(trackIndex: number): TrackEntry | null;
/** Adds a listener to receive events for all track entries. */
addListener(listener: AnimationStateListener): void;
/** Removes the listener added with {@link addListener}. */
removeListener(listener: AnimationStateListener): void;
/** Removes all listeners added with {@link addListener}. */
clearListeners(): void;
/** 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(): void;
}
/** Stores settings and other state for the playback of an animation on an {@link AnimationState} track.
*
* References to a track entry must not be kept after the {@link AnimationStateListener.dispose} event occurs. */
export declare class TrackEntry {
/** The animation to apply for this track entry. */
animation: Animation | null;
previous: TrackEntry | null;
/** The animation queued to start after this animation, or null. `next` makes up a linked list. */
next: TrackEntry | null;
/** The track entry for the previous animation when mixing to this animation, or null if no mixing is currently occurring.
* When mixing from multiple animations, `mixingFrom` makes up a doubly linked list. */
mixingFrom: TrackEntry | null;
/** The track entry for the next animation when mixing from this animation, or null if no mixing is currently occurring.
* When mixing to multiple animations, `mixingTo` makes up a doubly linked list. */
mixingTo: TrackEntry | null;
/** The listener for events generated by this track entry, or null.
*
* A track entry returned from {@link AnimationState.setAnimation} is already the current animation
* for the track, so the callback for listener {@link AnimationStateListener.start} will not be called. */
listener: AnimationStateListener | null;
/** The index of the track where this track entry is either current or queued.
*
* See {@link AnimationState.getTrack}. */
trackIndex: number;
/** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
* duration. */
loop: boolean;
/** When true, timelines in this animation that support additive have their values added to the setup or current pose values
* instead of replacing them. Additive can be set for a new track entry only before {@link AnimationState.apply}
* is next called. */
additive: boolean;
/** If true, the animation will be applied in reverse. */
reverse: boolean;
/** If true, mixing rotation between tracks always uses the shortest rotation direction. If the rotation is animated, the
* shortest rotation direction may change during the mix.
*
* If false, the shortest rotation direction is remembered when the mix starts and the same direction is used for the rest
* of the mix. Defaults to false.
*
* See {@link resetRotationDirections}. */
shortestRotation: boolean;
keepHold: boolean;
/** When the interpolated mix percentage is less than the `eventThreshold` , event timelines are applied while
* this animation is being mixed out. Defaults to 0, so event timelines are not applied while this animation is being mixed
* out. */
eventThreshold: number;
/** When the interpolated mix percentage is less than the `mixAttachmentThreshold`, attachment timelines are
* applied while this animation is being mixed out. Defaults to 0, so attachment timelines are not applied while this
* animation is being mixed out. */
mixAttachmentThreshold: number;
/** When the computed alpha is greater than `alphaAttachmentThreshold`, attachment timelines are applied. The
* computed alpha includes {@link alpha} and the interpolated mix percentage. Defaults to 0, so attachment timelines are
* always applied. */
alphaAttachmentThreshold: number;
/** When the interpolated mix percentage is less than the `mixAttachmentThreshold`, attachment timelines are
* applied while this animation is being mixed out. Defaults to 0, so attachment timelines are not applied while this
* animation is being mixed out. */
mixDrawOrderThreshold: number;
/** The time in seconds for the first frame of this animation, both initially and after looping. Defaults to 0.
*
* When setting `animationStart` time, {@link animationLast} can be set to the same value to avoid firing events
* from the start of the animation. */
animationStart: number;
/** The time in seconds for the last frame of this animation. Past this time, non-looping animations hold the pose at this
* time while looping animations will loop back to {@link animationStart}. Defaults to the {@link Animation.duration}. */
animationEnd: number;
/** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. For example, when
* this animation is applied, event timelines will fire all events between the `animationLast` time (exclusive)
* and `animationTime` (inclusive). Defaults to -1 to ensure triggers on frame 0 happen the first time this
* animation is applied. */
animationLast: number;
nextAnimationLast: number;
/** Seconds to postpone playing the animation. Must be >= 0. When this track entry is the current track entry,
* `delay` postpones incrementing the {@link trackTime}. When this track entry is queued, `delay` is
* the time from the start of the previous animation to when this track entry will become the current track entry (ie when
* the previous track entry {@link trackTime} >= this track entry's `delay`).
*
* {@link timeScale} affects the delay.
*
* When passing `delay` <= 0 to {@link AnimationState.addAnimation} this
* `delay` is set using a mix duration from {@link AnimationStateData}. To change the {@link mixDuration}
* afterward, use {@link setMixDuration} so this `delay` is adjusted. */
delay: number;
/** The time in seconds this track entry has been the current track entry, starting at 0 and increasing forever. Compare to
* {@link getAnimationTime}, which is always between {@link animationStart} and {@link animationEnd}.
*
* The track time can be set to start the animation at a time other than 0, without affecting looping. When doing so,
* {@link animationLast} can be set to the same value to avoid firing events from the start of the animation.
*
* To set the time an animation starts and loops, use {@link animationStart} and {@link animationEnd}. */
trackTime: number;
trackLast: number;
nextTrackLast: number;
/** The track time in seconds when this animation will be removed from the track. Defaults to the highest possible float
* value, meaning the animation will be applied until a new animation is set or the track is cleared. If the track end time
* is reached, no other animations are queued for playback, and mixing from any previous animations is complete, then the
* properties keyed by the animation are set to the setup pose and the track is cleared.
*
* Usually you want to use {@link AnimationState.addEmptyAnimation} rather than have the animation
* abruptly cease being applied, leaving the current pose. */
trackEnd: number;
/** Multiplier for the delta time when this track entry is updated, causing time for this animation to pass slower or
* faster. Defaults to 1.
*
* Values < 0 are not supported. To play an animation in reverse, use {@link reverse}.
*
* {@link mixTime} is not affected by track entry time scale, so {@link mixDuration} may need to be adjusted to match the
* animation speed.
*
* When using {@link AnimationState.addAnimation} with a `delay` <= 0, the
* {@link delay} is set using the mix duration from {@link AnimationState.data}, assuming time scale to be 1. If the time
* scale is not 1, the delay may need to be adjusted.
*
* See {@link AnimationState.timeScale} to affect all animations. */
timeScale: number;
/** Values < 1 mix this animation with the skeleton's current pose (either the setup pose or the pose from lower tracks).
* Defaults to 1, which overwrites the skeleton's current pose with this animation.
*
* Alpha should be 1 on track 0.
*
* See {@link getAlphaAttachmentThreshold}. */
alpha: number;
/** Seconds elapsed from 0 to the {@link mixDuration} when mixing from the previous animation to this animation. May
* be slightly more than `mixDuration` when the mix is complete. */
mixTime: number;
/** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
* {@link AnimationStateData.getMix} based on the animation before this animation (if any).
*
* A mix duration of 0 still needs to be applied one more time to mix out, so the the properties it was animating are
* reverted. A mix duration of 0 can be set at any time to end the mix on the next
* {@link AnimationState.update | update}.
*
* The `mixDuration` can be set manually rather than use the value from
* {@link AnimationStateData.getMix}. In that case, the `mixDuration` can be set for a new
* track entry only before {@link AnimationState.update} is next called.
*
* When using {@link AnimationState.addAnimation} with a `delay` <= 0, the
* {@link getDelay} is set using the mix duration from {@link AnimationState.data}. If `mixDuration` is set
* afterward, the delay needs to be adjusted:
*
* <pre>
* entry.mixDuration = 0.25;<br>
* entry.delay = entry.previous.getTrackComplete() - entry.mixDuration + 0;
* </pre>
*
* Alternatively, use {@link setMixDuration} to set both the mix duration and recompute the delay:<br>
*
* <pre>
entry.setMixDuration(0.25f, 0); // mixDuration, delay
* </pre>
*/
mixDuration: number;
totalAlpha: number;
mixInterpolation: Interpolation;
/** Sets both {@link getMixDuration} and {@link getDelay}.
* @param delay If > 0, sets {@link getDelay}. If <= 0, the delay set is the duration of the previous track entry minus
* the specified mix duration plus the specified `delay` (ie the mix ends at (when `delay` =
* 0) or before (when `delay` < 0) the previous track entry duration). If the previous entry is
* looping, its next loop completion is used instead of its duration. */
setMixDuration(mixDuration: number, delay?: number): void;
/** The interpolation to apply to the mix percentage ({@link mixTime} / {@link mixDuration}) when mixing from the previous
* animation to this animation. Defaults to linear. */
setMixInterpolation(mixInterpolation: Interpolation): void;
mix(): number;
/** For each timeline:
* - Bit 0, FIRST: 0 = mix from current pose, 1 = mix from setup pose. Timeline is first to set the property.
* - Bit 1, HOLD: 0 = mix out using alphaMix, 1 = apply full alpha to prevent dipping. Timeline is first on its track to
* set the property and the next entry (mixingTo) also sets it. When held, timelineHoldMix's mix controls how the hold fades
* out (for 3+ entry chains where the chain eventually stops setting the property). */
timelineMode: number[];
timelineHoldMix: TrackEntry[];
timelinesRotation: number[];
reset(): void;
/** Uses {@link trackTime} to compute the `animationTime`, which is always between {@link animationStart} and
* {@link animationEnd}. When `trackTime` is 0, `animationTime` is equal to the
* `animationStart` time. */
getAnimationTime(): number;
setAnimationLast(animationLast: number): void;
/** Returns true if at least one loop has been completed.
*
* See {@link AnimationStateListener.complete}. */
isComplete(): boolean;
/** When {@link shortestRotation} is false, this clears the directions for mixing this entry's rotation. This can be useful
* to avoid bones rotating the long way around when using {@link getAlpha} and starting animations on other tracks.
*
* Mixing involves finding a rotation between two others. There are two possible solutions: the short or the long way
* around. When the two rotations change over time, which direction is the short or long way can also change. If the short
* way was always chosen, bones 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. Resetting that direction makes it choose a new short way
* on the next apply. */
resetRotationDirections(): void;
/** If this track entry is non-looping, this is the track time in seconds when {@link animationEnd} is reached, or the
* current {@link trackTime} if it has already been reached.
*
* If this track entry is looping, this is the track time when this animation will reach its next {@link animationEnd} (the
* next loop completion). */
getTrackComplete(): number;
/** Returns true if this track entry has been applied at least once.
*
* See {@link AnimationState.apply}. */
wasApplied(): boolean;
/** Returns true if there is a {@link next} track entry and it will become the current track entry during the next
* {@link AnimationState.update}. */
isNextReady(): boolean;
}
export declare class EventQueue {
objects: Array<EventType | TrackEntry | Event>;
drainDisabled: boolean;
animState: AnimationState;
constructor(animState: AnimationState);
start(entry: TrackEntry): void;
interrupt(entry: TrackEntry): void;
end(entry: TrackEntry): void;
dispose(entry: TrackEntry): void;
complete(entry: TrackEntry): void;
event(entry: TrackEntry, event: Event): void;
drain(): void;
clear(): void;
}
export declare enum EventType {
start = 0,
interrupt = 1,
end = 2,
dispose = 3,
complete = 4,
event = 5
}
/** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving
* events.
*
* TrackEntry events are collected during {@link AnimationState.update} and {@link AnimationState.apply} and
* fired only after those methods are finished.
*
* See {@link TrackEntry.listener} and
* {@link AnimationState.addListener}. */
export interface AnimationStateListener {
/** Invoked when this entry has been set as the current entry. {@link end} will occur when this entry will no
* longer be applied.
*
* When this event is triggered by calling {@link AnimationState.setAnimation}, take care not to
* call {@link AnimationState.update} until after the TrackEntry has been configured. */
start?: (entry: TrackEntry) => void;
/** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for
* mixing. */
interrupt?: (entry: TrackEntry) => void;
/** Invoked when this entry will never be applied again. This only occurs if this entry has previously been set as the
* current entry ({@link start} was invoked). */
end?: (entry: TrackEntry) => void;
/** Invoked when this entry will be disposed. This may occur without the entry ever being set as the current entry.
* References to the entry should not be kept after dispose is called, as it may be destroyed or reused. */
dispose?: (entry: TrackEntry) => void;
/** Invoked every time this entry's animation completes a loop. This may occur during mixing (after
* {@link interrupt}).
*
* If this entry's {@link TrackEntry.mixingTo} is not null, this entry is mixing out (it is not the current entry).
*
* Because this event is triggered at the end of {@link AnimationState.apply}, any animations set in response to
* the event won't be applied until the next time the AnimationState is applied. */
complete?: (entry: TrackEntry) => void;
/** Invoked when this entry's animation triggers an event. */
event?: (entry: TrackEntry, event: Event) => void;
}
export declare abstract class AnimationStateAdapter implements AnimationStateListener {
start(entry: TrackEntry): void;
interrupt(entry: TrackEntry): void;
end(entry: TrackEntry): void;
dispose(entry: TrackEntry): void;
complete(entry: TrackEntry): void;
event(entry: TrackEntry, event: Event): void;
}
export declare const SUBSEQUENT = 0;
export declare const FIRST = 1;
export declare const HOLD = 2;
export declare const HOLD_FIRST = 3;
export declare const SETUP = 1;
export declare const RETAIN = 2;