UNPKG

@esotericsoftware/spine-core

Version:
412 lines (411 loc) 26.6 kB
/****************************************************************************** * 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;