@esotericsoftware/spine-core
Version:
The official Spine Runtimes for the web.
412 lines (411 loc) • 26.6 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.
*****************************************************************************/
import { Animation, MixBlend, AttachmentTimeline, RotateTimeline } from "./Animation.js";
import { AnimationStateData } from "./AnimationStateData.js";
import { Skeleton } from "./Skeleton.js";
import { Slot } from "./Slot.js";
import { StringSet, Pool } from "./Utils.js";
import { Event } from "./Event.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/) in the Spine Runtimes Guide. */
export declare class AnimationState {
static _emptyAnimation: Animation;
private static emptyAnimation;
/** The AnimationStateData to look up mix durations. */
data: AnimationStateData;
/** The list of tracks that currently have animations, which may contain null entries. */
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 TrackEntry {@link TrackEntry#timeScale} for affecting a single animation. */
timeScale: number;
unkeyedState: number;
events: Event[];
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. 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: Skeleton): boolean;
applyMixingFrom(to: TrackEntry, skeleton: Skeleton, blend: MixBlend): number;
applyAttachmentTimeline(timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, attachments: boolean): void;
setAttachment(skeleton: Skeleton, slot: Slot, attachmentName: string | null, attachments: boolean): void;
applyRotateTimeline(timeline: RotateTimeline, skeleton: Skeleton, time: number, alpha: number, blend: MixBlend, timelinesRotation: Array<number>, i: number, firstFrame: boolean): void;
queueEvents(entry: TrackEntry, animationTime: number): void;
/** 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(): void;
/** 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: number): void;
setCurrent(index: number, current: TrackEntry, interrupt: boolean): void;
/** Sets an animation by name.
*
* See {@link #setAnimationWith()}. */
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 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: number, animation: Animation, loop?: boolean): TrackEntry;
/** Queues an animation by name.
*
* See {@link #addAnimationWith()}. */
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 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: number, animation: Animation, loop?: boolean, delay?: number): TrackEntry;
/** 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: 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 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: 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. */
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 the {@link TrackEntry#getNext() next entry} 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. */
getCurrent(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 from the previous animation to this animation, or null if no
* mixing is currently occuring. When mixing from multiple animations, `mixingFrom` makes up a linked list. */
mixingFrom: TrackEntry | null;
/** The track entry for the next animation when mixing from this animation to the next animation, or null if no mixing is
* currently occuring. When mixing to multiple animations, `mixingTo` makes up a 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 track entry 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#getCurrent()}. */
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;
/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
* of being mixed out.
*
* When mixing between animations that key the same property, if a lower track also keys that property then the value will
* briefly dip toward the lower track value during the mix. This happens because the first animation mixes from 100% to 0%
* while the second animation mixes from 0% to 100%. Setting `holdPrevious` to true applies the first animation
* at 100% during the mix so the lower track value is overwritten. Such dipping does not occur on the lowest track which
* keys the property, only when a higher track also keys the property.
*
* Snapping will occur if `holdPrevious` is true and this animation does not key all the same properties as the
* previous animation. */
holdPrevious: boolean;
reverse: boolean;
shortestRotation: boolean;
/** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) 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 mix percentage ({@link #mixtime} / {@link #mixDuration}) is less than the
* `attachmentThreshold`, 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 {@link #getAlpha()} is greater than <code>alphaAttachmentThreshold</code>, attachment timelines are applied.
* Defaults to 0, so attachment timelines are always applied. */
alphaAttachmentThreshold: number;
/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
* <code>mixDrawOrderThreshold</code>, draw order timelines are applied while this animation is being mixed out. Defaults to
* 0, so draw order timelines are not applied while this animation is being mixed out. */
mixDrawOrderThreshold: number;
/** Seconds when this animation starts, both initially and after looping. Defaults to 0.
*
* When changing the `animationStart` time, it often makes sense to set {@link #animationLast} to the same
* value to prevent timeline keys before the start time from triggering. */
animationStart: number;
/** Seconds for the last frame of this animation. Non-looping animations won't play past this time. Looping animations will
* loop back to {@link #animationStart} at this time. Defaults to the animation {@link Animation#duration}. */
animationEnd: number;
/** The time in seconds this animation was last applied. Some timelines use this for one-time triggers. Eg, 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. 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 TrackEntry#trackTime} >= this track entry's `delay`).
*
* {@link #timeScale} affects the delay. */
delay: number;
/** Current time in seconds this track entry has been the current track entry. The track time determines
* {@link #animationTime}. The track time can be set to start the animation at a time other than 0, without affecting
* looping. */
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.
*
* It may be desired to use {@link AnimationState#addEmptyAnimation()} rather than have the animation
* abruptly cease being applied. */
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.
*
* {@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, note the
* {@link #delay} is set using the mix duration from the {@link AnimationStateData}, assuming time scale to be 1. If
* the time scale is not 1, the delay may need to be adjusted.
*
* See AnimationState {@link AnimationState#timeScale} for affecting all animations. */
timeScale: number;
/** Values < 1 mix this animation with the skeleton's current pose (usually the pose resulting from lower tracks). Defaults
* to 1, which overwrites the skeleton's current pose with this animation.
*
* Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to
* use alpha on track 0 if the skeleton pose is from the last frame render. */
alpha: number;
/** Seconds from 0 to the {@link #getMixDuration()} 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 AnimationStateData
* {@link AnimationStateData#getMix()} based on the animation before this animation (if any).
*
* A mix duration of 0 still mixes out over one frame to provide the track entry being mixed out a chance to revert the
* properties it was animating.
*
* 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(float)} is first called.
*
* When using {@link AnimationState#addAnimation()} with a `delay` <= 0, note the
* {@link #delay} is set using the mix duration from the {@link AnimationStateData}, not a mix duration set
* afterward. */
_mixDuration: number;
interruptAlpha: number;
totalAlpha: number;
get mixDuration(): number;
set mixDuration(mixDuration: number);
setMixDurationWithDelay(mixDuration: number, delay: number): void;
/** 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. */
mixBlend: MixBlend;
timelineMode: number[];
timelineHoldMix: TrackEntry[];
timelinesRotation: number[];
reset(): void;
/** 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(): number;
setAnimationLast(animationLast: number): void;
/** Returns true if at least one loop has been completed.
*
* See {@link AnimationStateListener#complete()}. */
isComplete(): boolean;
/** 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(): void;
getTrackComplete(): number;
/** Returns true if this track entry has been applied at least once.
* <p>
* See {@link AnimationState#apply(Skeleton)}. */
wasApplied(): boolean;
/** Returns true if there is a {@link #getNext()} track entry and it will become the current track entry during the next
* {@link AnimationState#update(float)}. */
isNextReady(): boolean;
}
export declare class EventQueue {
objects: Array<any>;
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.
*
* See TrackEntry {@link TrackEntry#listener} and AnimationState
* {@link AnimationState#addListener()}. */
export interface AnimationStateListener {
/** Invoked when this entry has been set as the current entry. */
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 is no longer the current entry and will never be applied again. */
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. */
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;
}
/** 1. A previously applied timeline has set this property.
*
* Result: Mix from the current pose to the timeline pose. */
export declare const 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. */
export declare const 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. */
export declare const 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. */
export declare const 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. */
export declare const HOLD_MIX = 4;
export declare const SETUP = 1;
export declare const CURRENT = 2;