[ts] Ported latest changes from 29bf262d to b116c42f.

This commit is contained in:
Davide Tantillo 2026-03-26 11:51:10 +01:00
parent b116c42f8b
commit 9609fe6e5e
62 changed files with 1644 additions and 1643 deletions

View File

@ -56,7 +56,7 @@ export class SkeletonRenderer {
const ctx = this.ctx;
const color = this.tempColor;
const skeletonColor = skeleton.color;
const drawOrder = skeleton.drawOrder;
const drawOrder = skeleton.drawOrder.appliedPose;
if (this.debugRendering) ctx.strokeStyle = "green";
@ -65,7 +65,7 @@ export class SkeletonRenderer {
const bone = slot.bone;
if (!bone.active) continue;
const pose = slot.applied;
const pose = slot.appliedPose;
const attachment = pose.attachment;
if (!(attachment instanceof RegionAttachment)) continue;
@ -85,7 +85,7 @@ export class SkeletonRenderer {
skeletonColor.a * slotColor.a * regionColor.a);
ctx.save();
const boneApplied = bone.applied;
const boneApplied = bone.appliedPose;
ctx.transform(boneApplied.a, boneApplied.c, boneApplied.b, boneApplied.d, boneApplied.worldX, boneApplied.worldY);
const offsets = attachment.getOffsets(pose);
ctx.translate(offsets[0], offsets[1]);
@ -116,7 +116,7 @@ export class SkeletonRenderer {
const ctx = this.ctx;
const color = this.tempColor;
const skeletonColor = skeleton.color;
const drawOrder = skeleton.drawOrder;
const drawOrder = skeleton.drawOrder.appliedPose;
let blendMode: BlendMode | null = null;
let vertices: ArrayLike<number> = this.vertices;
@ -124,7 +124,7 @@ export class SkeletonRenderer {
for (let i = 0, n = drawOrder.length; i < n; i++) {
const slot = drawOrder[i];
const pose = slot.applied;
const pose = slot.appliedPose;
const attachment = pose.attachment;
let texture: HTMLImageElement;
@ -243,7 +243,7 @@ export class SkeletonRenderer {
private computeRegionVertices (slot: Slot, region: RegionAttachment, offsets: NumberArrayLike, uvs: NumberArrayLike, pma: boolean) {
const skeletonColor = slot.skeleton.color;
const slotColor = slot.applied.color;
const slotColor = slot.appliedPose.color;
const regionColor = region.color;
const alpha = skeletonColor.a * slotColor.a * regionColor.a;
const multiplier = pma ? alpha : 1;
@ -291,7 +291,7 @@ export class SkeletonRenderer {
private computeMeshVertices (slot: Slot, mesh: MeshAttachment, uvs: NumberArrayLike, pma: boolean) {
const skeleton = slot.skeleton;
const skeletonColor = skeleton.color;
const slotColor = slot.applied.color;
const slotColor = slot.appliedPose.color;
const regionColor = mesh.color;
const alpha = skeletonColor.a * slotColor.a * regionColor.a;
const multiplier = pma ? alpha : 1;

File diff suppressed because it is too large Load Diff

View File

@ -29,7 +29,7 @@
/** biome-ignore-all lint/style/noNonNullAssertion: reference runtime expects some nullable to not be null */
import { Animation, AttachmentTimeline, DrawOrderFolderTimeline, DrawOrderTimeline, EventTimeline, MixBlend, MixDirection, RotateTimeline, Timeline } from "./Animation.js";
import { Animation, AttachmentTimeline, DrawOrderFolderTimeline, DrawOrderTimeline, RotateTimeline, Timeline } from "./Animation.js";
import type { AnimationStateData } from "./AnimationStateData.js";
import type { Event } from "./Event.js";
import type { Skeleton } from "./Skeleton.js";
@ -40,27 +40,27 @@ import { MathUtils, Pool, StringSet, Utils } 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/) in the Spine Runtimes Guide. */
* See [Applying Animations](http://esotericsoftware.com/spine-applying-animations#AnimationState-API) in the Spine Runtimes Guide. */
export class AnimationState {
static readonly emptyAnimation = new Animation("<empty>", [], 0);
/** The AnimationStateData to look up mix durations. */
data: AnimationStateData;
/** The list of tracks that currently have animations, which may contain null entries. */
/** The list of tracks that have had animations. May contain null entries for tracks that currently have no animation. */
readonly tracks = [] as (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. */
* See {@link TrackEntry#timeScale} to affect a single animation. */
timeScale = 1;
unkeyedState = 0;
readonly events = [] as Event[];
readonly listeners = [] as AnimationStateListener[];
queue = new EventQueue(this);
propertyIDs = new StringSet();
propertyIds = new StringSet();
animationsChanged = false;
trackEntryPool = new Pool<TrackEntry>(() => new TrackEntry());
@ -69,7 +69,7 @@ export class AnimationState {
this.data = data;
}
/** Increments each track entry {@link TrackEntry#trackTime()}, setting queued animations as current if needed. */
/** Increments each track entry {@link TrackEntry#trackTime}, setting queued animations as current if needed. */
update (delta: number) {
delta *= this.timeScale;
const tracks = this.tracks;
@ -143,7 +143,10 @@ export class AnimationState {
if (from.totalAlpha === 0 || to.mixDuration === 0) {
to.mixingFrom = from.mixingFrom;
if (from.mixingFrom != null) from.mixingFrom.mixingTo = to;
to.interruptAlpha = from.interruptAlpha;
if (from.totalAlpha === 0) {
for (let next = to; next.mixingTo != null; next = next.mixingTo)
next.keepHold = true;
}
this.queue.end(from);
}
return finished;
@ -170,17 +173,12 @@ export class AnimationState {
if (!current || current.delay > 0) continue;
applied = true;
// Track 0 animations aren't for layering, so never use current values before the first key.
const blend: MixBlend = i === 0 ? MixBlend.first : current.mixBlend;
// Apply mixing from entries first.
let alpha = current.alpha;
if (current.mixingFrom)
alpha *= this.applyMixingFrom(current, skeleton);
else if (current.trackTime >= current.trackEnd && !current.next)
alpha = 0;
let attachments = alpha >= current.alphaAttachmentThreshold;
// Apply current entry.
let animationLast = current.animationLast, animationTime = current.getAnimationTime(), applyTime = animationTime;
@ -191,37 +189,36 @@ export class AnimationState {
}
const timelines = current.animation!.timelines;
const timelineCount = timelines.length;
if ((i === 0 && alpha === 1) || blend === MixBlend.add) {
if (i === 0) attachments = true;
if ((i === 0 && alpha === 1)) {
for (let ii = 0; ii < timelineCount; ii++) {
// Fixes issue #302 on IOS9 where mix, blend sometimes became undefined and caused assets
// to sometimes stop rendering when using color correction, as their RGBA values become NaN.
// (https://github.com/pixijs/pixi-spine/issues/302)
Utils.webkit602BugfixHelper(alpha, blend);
Utils.webkit602BugfixHelper(alpha);
const timeline = timelines[ii];
if (timeline instanceof AttachmentTimeline)
this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, false, attachments);
this.applyAttachmentTimeline(timeline, skeleton, applyTime, true, false, true);
else
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false);
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, true, false, false, false);
}
} else {
const timelineMode = current.timelineMode;
const shortestRotation = current.shortestRotation;
const attachments = alpha >= current.alphaAttachmentThreshold;
const add = current.additive, shortestRotation = add || current.shortestRotation;
const firstFrame = !shortestRotation && current.timelinesRotation.length !== timelineCount << 1;
if (firstFrame) current.timelinesRotation.length = timelineCount << 1;
for (let ii = 0; ii < timelineCount; ii++) {
const timeline = timelines[ii];
const timelineBlend = timelineMode[ii] === SUBSEQUENT ? current.mixBlend : MixBlend.setup;
const fromSetup = (timelineMode[ii] & FIRST) !== 0;
if (!shortestRotation && timeline instanceof RotateTimeline) {
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, current.timelinesRotation, ii << 1, firstFrame);
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, fromSetup, current.timelinesRotation, ii << 1, firstFrame);
} else if (timeline instanceof AttachmentTimeline) {
this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, false, attachments);
this.applyAttachmentTimeline(timeline, skeleton, applyTime, fromSetup, false, attachments);
} else {
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
Utils.webkit602BugfixHelper(alpha, blend);
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.in, false);
Utils.webkit602BugfixHelper(alpha);
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, fromSetup, add, false, false);
}
}
}
@ -251,103 +248,75 @@ export class AnimationState {
applyMixingFrom (to: TrackEntry, skeleton: Skeleton) {
const from = to.mixingFrom!;
if (from.mixingFrom) this.applyMixingFrom(from, skeleton);
const fromMix = from.mixingFrom !== null ? this.applyMixingFrom(from, skeleton) : 1;
const mix: number = to.mixDuration === 0 ? 1 : Math.min(1, to.mixTime / to.mixDuration);
let mix = 0;
if (to.mixDuration === 0) // Single frame mix to undo mixingFrom changes.
mix = 1;
else {
mix = to.mixTime / to.mixDuration;
if (mix > 1) mix = 1;
}
const a = from.alpha * fromMix, keep = 1 - mix * to.alpha;
const alphaMix = a * (1 - mix), alphaHold = keep > 0 ? alphaMix / keep : a;
const attachments = mix < from.mixAttachmentThreshold, drawOrder = mix < from.mixDrawOrderThreshold;
const timelines = from.animation!.timelines;
const timelineCount = timelines.length;
const alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
const timelineMode = from.timelineMode;
const timelineHoldMix = from.timelineHoldMix;
const attachments = mix < from.mixAttachmentThreshold, drawOrder = mix < from.mixDrawOrderThreshold;
const add = from.additive, shortestRotation = add || from.shortestRotation;
const firstFrame = !shortestRotation && from.timelinesRotation.length !== timelineCount << 1;
if (firstFrame) from.timelinesRotation.length = timelineCount << 1;
const timelinesRotation = from.timelinesRotation;
let animationLast = from.animationLast, animationTime = from.getAnimationTime(), applyTime = animationTime;
let events = null;
if (from.reverse)
applyTime = from.animation!.duration - applyTime;
else if (mix < from.eventThreshold)
else if (mix < from.eventThreshold) //
events = this.events;
const blend = from.mixBlend;
if (blend === MixBlend.add) {
for (let i = 0; i < timelineCount; i++)
timelines[i].apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out, false);
} else {
const timelineMode = from.timelineMode;
const timelineHoldMix = from.timelineHoldMix;
from.totalAlpha = 0;
const shortestRotation = from.shortestRotation;
const firstFrame = !shortestRotation && from.timelinesRotation.length !== timelineCount << 1;
if (firstFrame) from.timelinesRotation.length = timelineCount << 1;
from.totalAlpha = 0;
for (let i = 0; i < timelineCount; i++) {
const timeline = timelines[i];
let timelineBlend: MixBlend;
let alpha = 0;
switch (timelineMode[i]) {
case SUBSEQUENT:
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
timelineBlend = blend;
alpha = alphaMix;
break;
case FIRST:
timelineBlend = MixBlend.setup;
alpha = alphaMix;
break;
case HOLD_SUBSEQUENT:
timelineBlend = blend;
alpha = alphaHold;
break;
case HOLD_FIRST:
timelineBlend = MixBlend.setup;
alpha = alphaHold;
break;
default: { // HOLD_MIX
timelineBlend = MixBlend.setup;
const holdMix = timelineHoldMix[i];
alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
break;
}
}
from.totalAlpha += alpha;
if (!shortestRotation && timeline instanceof RotateTimeline)
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, from.timelinesRotation, i << 1, firstFrame);
else if (timeline instanceof AttachmentTimeline)
this.applyAttachmentTimeline(timeline, skeleton, applyTime, timelineBlend, true, attachments && alpha >= from.alphaAttachmentThreshold);
else {
let direction = MixDirection.out;
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
Utils.webkit602BugfixHelper(alpha, blend);
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend === MixBlend.setup)
direction = MixDirection.in;
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false);
}
for (let i = 0; i < timelineCount; i++) {
const timeline = timelines[i];
const mode = timelineMode[i];
let alpha = 0;
if ((mode & HOLD) !== 0) {
const holdMix = timelineHoldMix[i];
alpha = holdMix == null ? alphaHold : alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration);
} else {
if (!drawOrder && timeline instanceof DrawOrderTimeline) continue;
alpha = alphaMix;
}
from.totalAlpha += alpha;
const fromSetup = (mode & FIRST) !== 0;
if (!shortestRotation && timeline instanceof RotateTimeline) {
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, fromSetup, timelinesRotation, i << 1, firstFrame);
} else if (timeline instanceof AttachmentTimeline)
this.applyAttachmentTimeline(timeline, skeleton, applyTime, fromSetup, true,
attachments && alpha >= from.alphaAttachmentThreshold);
else {
const out = !drawOrder || !(timeline instanceof DrawOrderTimeline) || !fromSetup;
timeline.apply(skeleton, animationLast, applyTime, events, alpha, fromSetup, add, out, false);
}
}
if (to.mixDuration > 0) this.queueEvents(from, animationTime);
this.events.length = 0;
from.nextAnimationLast = animationTime;
from.nextTrackLast = from.trackTime;
return mix;
}
applyAttachmentTimeline (timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, out: boolean, attachments: boolean) {
/** Applies the attachment timeline and sets {@link Slot#attachmentState}.
* @param attachments False when: 1) the attachment timeline is mixing out, 2) mix < attachmentThreshold, and 3) the timeline
* is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
* timelines see any deform. */
applyAttachmentTimeline (timeline: AttachmentTimeline, skeleton: Skeleton, time: number, fromSetup: boolean,
out: boolean, attachments: boolean) {
const slot = skeleton.slots[timeline.slotIndex];
if (!slot.bone.active) return;
if (out) {
if (blend === MixBlend.setup) this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else if (time < timeline.frames[0]) { // Time is before first frame.
if (blend === MixBlend.setup || blend === MixBlend.first)
this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
if (out || time < timeline.frames[0]) {
if (fromSetup) this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else
this.setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search(timeline.frames, time)], attachments);
@ -360,37 +329,28 @@ export class AnimationState {
if (attachments) slot.attachmentState = this.unkeyedState + CURRENT;
}
applyRotateTimeline (timeline: RotateTimeline, skeleton: Skeleton, time: number, alpha: number, blend: MixBlend,
/** 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) {
if (firstFrame) timelinesRotation[i] = 0;
if (alpha === 1) {
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in, false);
timeline.apply(skeleton, 0, time, null, 1, fromSetup, false, false, false);
return;
}
const bone = skeleton.bones[timeline.boneIndex];
if (!bone.active) return;
const pose = bone.pose, setup = bone.data.setup;
const pose = bone.pose, setup = bone.data.setupPose;
const frames = timeline.frames;
let r1 = 0, r2 = 0;
if (time < frames[0]) {
switch (blend) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime does fall through
case MixBlend.setup:
pose.rotation = setup.rotation;
// biome-ignore lint/suspicious/useDefaultSwitchClauseLast: needed for fall through
default:
return;
case MixBlend.first:
r1 = pose.rotation;
r2 = setup.rotation;
}
} else {
r1 = blend === MixBlend.setup ? setup.rotation : pose.rotation;
r2 = setup.rotation + timeline.getCurveValue(time);
if (time < frames[0]) { // Time is before first frame.
if (fromSetup) pose.rotation = setup.rotation;
return;
}
const r1 = fromSetup ? setup.rotation : pose.rotation;
const r2 = setup.rotation + timeline.getCurveValue(time);
// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
let total = 0, diff = r2 - r1;
@ -463,8 +423,8 @@ export class AnimationState {
/** 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. */
* Usually you want to use {@link #setEmptyAnimations(float)} 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;
@ -477,8 +437,8 @@ export class AnimationState {
/** 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. */
* Usually you want to use {@link #setEmptyAnimation(int, float)} to mix the skeletons back to the setup pose, rather than
* leaving them in their current pose. */
clearTrack (trackIndex: number) {
if (trackIndex >= this.tracks.length) return;
const current = this.tracks[trackIndex];
@ -514,10 +474,6 @@ export class AnimationState {
from.mixingTo = current;
current.mixTime = 0;
// Store the interrupted mix percentage.
if (from.mixingFrom && from.mixDuration > 0)
current.interruptAlpha *= Math.min(1, from.mixTime / from.mixDuration);
from.timelinesRotation.length = 0; // Reset rotation for mixing out, in case entry was mixed in.
}
@ -552,7 +508,7 @@ export class AnimationState {
}
/** Sets the current animation for a track, discarding any queued animations.
* <p>
*
* 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
@ -590,8 +546,8 @@ export class AnimationState {
/** 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 the {@link AnimationStateData}) plus the specified <code>delay</code> (ie the mix
* ends at (<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the
* 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. */
@ -641,15 +597,15 @@ export class AnimationState {
* {@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.
* 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()} 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.
* {@link #addAnimation(int, Animation, boolean, float)} with the desired delay (an empty animation has a duration of 0) and on
* the returned track entry, set the {@link TrackEntry#setMixDuration(float)}. 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
* See <a href='https://esotericsoftware.com/spine-applying-animations#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide. */
setEmptyAnimation (trackIndex: number, mixDuration: number = 0) {
const entry = this.setAnimation(trackIndex, AnimationState.emptyAnimation, false);
@ -659,16 +615,16 @@ export class AnimationState {
}
/** 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#getMixDuration()}. If the track has no entries, it is equivalent to calling
* {@link TrackEntry#mixDuration}. If the track has no entries, it is equivalent to calling
* {@link #setEmptyAnimation(int, float)}.
*
* See {@link #setEmptyAnimation(int, float)} 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#getDelay()}. If <= 0, the delay set is the duration of the previous track entry
* minus any mix duration plus the specified <code>delay</code> (ie the mix ends at (<code>delay</code> = 0) or
* before (<code>delay</code> < 0) the previous track entry duration). If the previous entry is looping, its next
* loop completion is used instead of its duration.
* <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 <code>delay</code> (ie the mix ends at (when <code>delay</code> = 0) or before
* (when <code>delay</code> < 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(TrackEntry)} event occurs. */
addEmptyAnimation (trackIndex: number, mixDuration: number = 0, delay: number = 0) {
@ -681,7 +637,7 @@ export class AnimationState {
/** 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
* See <a href='https://esotericsoftware.com/spine-applying-animations#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide. */
setEmptyAnimations (mixDuration: number = 0) {
const oldDrainDisabled = this.queue.drainDisabled;
@ -708,8 +664,8 @@ export class AnimationState {
entry.trackIndex = trackIndex;
entry.animation = animation;
entry.loop = loop;
entry.holdPrevious = false;
entry.additive = false;
entry.reverse = false;
entry.shortestRotation = false;
@ -733,13 +689,12 @@ export class AnimationState {
entry.alpha = 1;
entry.mixTime = 0;
entry.mixDuration = !last ? 0 : this.data.getMix(last.animation!, animation);
entry.interruptAlpha = 1;
entry.totalAlpha = 0;
entry.mixBlend = MixBlend.replace;
entry.keepHold = false;
return entry;
}
/** Removes the {@link TrackEntry#getNext() next entry} and all entries after it for the specified entry. */
/** Removes {@link TrackEntry#next} and all entries after it for the specified entry. */
clearNext (entry: TrackEntry) {
let next = entry.next;
while (next) {
@ -752,7 +707,6 @@ export class AnimationState {
_animationsChanged () {
this.animationsChanged = false;
this.propertyIDs.clear();
const tracks = this.tracks;
for (let i = 0, n = tracks.length; i < n; i++) {
let entry = tracks[i];
@ -760,56 +714,60 @@ export class AnimationState {
while (entry.mixingFrom)
entry = entry.mixingFrom;
do {
if (!entry.mixingTo || entry.mixBlend !== MixBlend.add) this.computeHold(entry);
this.computeHold(entry);
entry = entry.mixingTo;
} while (entry);
}
this.propertyIds.clear();
}
computeHold (entry: TrackEntry) {
const to = entry.mixingTo;
const timelines = entry.animation!.timelines;
const timelinesCount = entry.animation!.timelines.length;
const timelineMode = entry.timelineMode;
timelineMode.length = timelinesCount;
const timelineHoldMix = entry.timelineHoldMix;
timelineHoldMix.length = 0;
const propertyIDs = this.propertyIDs;
if (to?.holdPrevious) {
for (let i = 0; i < timelinesCount; i++) {
let first = propertyIDs.addAll(timelines[i].getPropertyIds());
if (first && timelines[i] instanceof DrawOrderFolderTimeline && propertyIDs.contains(DrawOrderTimeline.propertyID))
first = false; // DrawOrderTimeline changed.
timelineMode[i] = first ? HOLD_FIRST : HOLD_SUBSEQUENT;
}
return;
}
const propertyIds = this.propertyIds;
const add = entry.additive, keepHold = entry.keepHold;
const to = entry.mixingTo;
outer:
for (let i = 0; i < timelinesCount; i++) {
const timeline = timelines[i];
const ids = timeline.getPropertyIds();
if (!propertyIDs.addAll(ids))
timelineMode[i] = SUBSEQUENT;
else if (timeline instanceof DrawOrderFolderTimeline && propertyIDs.contains(DrawOrderTimeline.propertyID))
timelineMode[i] = SUBSEQUENT; // DrawOrderTimeline changed.
else if (!to || timeline instanceof AttachmentTimeline || timeline instanceof DrawOrderTimeline
|| timeline instanceof DrawOrderFolderTimeline || timeline instanceof EventTimeline
|| !to.animation!.hasTimeline(ids)) {
timelineMode[i] = FIRST;
} else {
for (let next = to.mixingTo; next; next = next!.mixingTo) {
if (next.animation!.hasTimeline(ids)) continue;
if (entry.mixDuration > 0) {
timelineMode[i] = HOLD_MIX;
timelineHoldMix[i] = next;
continue outer;
}
break;
}
timelineMode[i] = HOLD_FIRST;
const ids = timeline.propertyIds;
const first = propertyIds.addAll(ids)
&& !(timeline instanceof DrawOrderFolderTimeline && propertyIds.contains(DrawOrderTimeline.propertyID));
if (add && timeline.additive) {
timelineMode[i] = first ? FIRST : SUBSEQUENT;
continue;
}
for (let from = entry.mixingFrom; from != null; from = from.mixingFrom) {
if (from.animation!.hasTimeline(ids)) {
// An earlier entry on this track keys this property, isolating it from lower tracks.
timelineMode[i] = SUBSEQUENT;
continue outer;
}
}
// Hold if the next entry will overwrite this property.
let mode: number;
if (to === null || timeline.instant || (to.additive && timeline.additive) || !to.animation?.hasTimeline(ids))
mode = first ? FIRST : SUBSEQUENT;
else {
mode = first ? HOLD_FIRST : HOLD;
// Find next entry that doesn't overwrite this property. Its mix fades out the hold, instead of it ending abruptly.
for (let next = to.mixingTo; next != null; next = next.mixingTo) {
if ((next.additive && timeline.additive) || !next.animation?.hasTimeline(ids)) {
if (next.mixDuration > 0) timelineHoldMix[i] = next;
break;
}
}
}
if (keepHold) mode = (mode & ~HOLD) | (timelineMode[i] & HOLD);
timelineMode[i] = mode;
}
}
@ -856,173 +814,190 @@ export class TrackEntry {
/** The animation queued to start after this animation, or null. `next` makes up a linked list. */
next: TrackEntry | null = 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. */
/** 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, <code>mixingFrom</code> makes up a doubly linked list. */
mixingFrom: TrackEntry | null = 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. */
/** 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, <code>mixingTo</code> makes up a doubly linked list. */
mixingTo: TrackEntry | null = 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. */
* for the track, so the callback for listener {@link AnimationStateListener#start()} will not be called. */
listener: AnimationStateListener | null = null;
/** The index of the track where this track entry is either current or queued.
*
* See {@link AnimationState#getCurrent()}. */
trackIndex: number = 0;
trackIndex = 0;
/** If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
* duration. */
loop: boolean = false;
loop = false;
/** If true, when mixing from the previous animation to this animation, the previous animation is applied as normal instead
* of being mixed out.
/** 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(Skeleton)}
* is next called. */
additive = false;
/** If true, the animation will be applied in reverse and events will not be fired. */
reverse = false;
/** 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.
*
* 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.
* 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.
*
* Snapping will occur if `holdPrevious` is true and this animation does not key all the same properties as the
* previous animation. */
holdPrevious: boolean = false;
* See {@link #resetRotationDirections()}. */
shortestRotation = false;
reverse: boolean = false;
keepHold = false;
shortestRotation: boolean = false;
/** 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 = 0;
/** 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 = 0;
/** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) 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 = 0;
/** 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 = 0;
/** When the computed alpha is greater than `alphaAttachmentThreshold`, attachment timelines are applied. The
* computed alpha includes {@link #alpha} and the mix percentage. Defaults to 0, so attachment timelines are always
* applied. */
alphaAttachmentThreshold = 0;
/** When {@link #getAlpha()} is greater than <code>alphaAttachmentThreshold</code>, attachment timelines are applied.
* Defaults to 0, so attachment timelines are always applied. */
alphaAttachmentThreshold: number = 0;
/** 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 = 0;
/** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the `mixDrawOrderThreshold`,
* 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 = 0;
/** 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 = 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 = 0;
/** 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 = 0;
animationEnd = 0;
/** 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 = 0;
animationLast = 0;
nextAnimationLast: number = 0;
nextAnimationLast = 0;
/** 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`).
/** 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. */
delay: number = 0;
* {@link #timeScale} affects the delay.
*
* When passing `delay` <= 0 to {@link AnimationState#addAnimation(int, Animation, boolean, float)} this
* `delay` is set using a mix duration from {@link AnimationStateData}. To change the {@link #mixDuration}
* afterward, use {@link #setMixDuration(float, float)} so this `delay` is adjusted. */
delay = 0;
/** 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 = 0;
trackTime = 0;
trackLast: number = 0; nextTrackLast: number = 0;
trackLast = 0; nextTrackLast = 0;
/** 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
* Usually you want to use {@link AnimationState#addEmptyAnimation()} rather than have the animation
* abruptly cease being applied. */
trackEnd: number = 0;
trackEnd = 0;
/** 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.
* Values < 0 are not supported. To play an animation in reverse, use {@link #reverse}.
*
* 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.
* {@link #mixTime} is not affected by track entry time scale, so {@link #mixDuration} may need to be adjusted to match the
* animation speed.
*
* See AnimationState {@link AnimationState#timeScale} for affecting all animations. */
timeScale: number = 0;
* When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} 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 = 0;
/** 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.
/** 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.
*
* 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.
* @see alphaAttachmentThreshold */
alpha: number = 0;
/** 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 = 0;
/** 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).
* Alpha should be 1 on track 0.
*
* 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.
* See {@link #getAlphaAttachmentThreshold()}. */
alpha = 0;
/** 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 = 0;
/** Seconds for mixing from the previous animation to this animation. Defaults to the value provided by
* {@link AnimationStateData#getMix(Animation, Animation)} 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(float)
* 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
* {@link AnimationStateData#getMix(Animation, Animation)}. In that case, the `mixDuration` can be set for a new
* track entry only before {@link AnimationState#update(float)} is next 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 = 0;
* When using {@link AnimationState#addAnimation(int, Animation, boolean, float)} 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(float, float)} to set both the mix duration and recompute the delay:<br>
*
* <pre>
entry.setMixDuration(0.25f, 0); // mixDuration, delay
* </pre>
*/
mixDuration = 0;
interruptAlpha: number = 0; totalAlpha: number = 0;
totalAlpha = 0;
/** Sets both {@link #getMixDuration()} and {@link #getDelay()}.
* @param delay If > 0, sets {@link TrackEntry#getDelay()}. If <= 0, the delay set is the duration of the previous track
* entry minus the specified mix duration plus the specified <code>delay</code> (ie the mix ends at
* (<code>delay</code> = 0) or before (<code>delay</code> < 0) the previous track entry duration). If the previous
* entry is looping, its next loop completion is used instead of its duration. */
* @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 <code>delay</code> (ie the mix ends at (when <code>delay</code> =
* 0) or before (when <code>delay</code> < 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) {
this.mixDuration = mixDuration;
if (delay !== undefined) {
if (delay <= 0) {
if (this.previous != null)
delay = Math.max(delay + this.previous.getTrackComplete() - mixDuration, 0);
else
delay = 0;
}
if (delay <= 0) delay = this.previous == null ? 0 : Math.max(delay + this.previous.getTrackComplete() - mixDuration, 0);
this.delay = delay;
}
}
/** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}.
*
* The `mixBlend` can be set for a new track entry only before {@link AnimationState#apply()} is next
* called. */
mixBlend = MixBlend.replace;
/** For each timeline:
* <li>Bit 0, FIRST: 0 = mix from current pose, 1 = mix from setup pose. Timeline is first to set the property.
* <li>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 = [] as number[];
timelineHoldMix = [] as TrackEntry[];
timelinesRotation = [] as number[];
@ -1039,9 +1014,12 @@ export class TrackEntry {
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. */
/** Uses {@link #trackTime} to compute the `animationTime`. When the `trackTime` is 0, the
* `animationTime` is equal to the `animationStart` time.
*
* The `animationTime` is between {@link #animationStart} and {@link #animationEnd}, except if this track entry
* is non-looping and {@link #animationEnd} is >= to the {@link Animation#duration}, then `animationTime`
* continues to increase past {@link #animationEnd}. */
getAnimationTime () {
if (this.loop) {
const duration = this.animationEnd - this.animationStart;
@ -1063,17 +1041,23 @@ export class TrackEntry {
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.
/** 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 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. */
* 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 () {
this.timelinesRotation.length = 0;
}
/** 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 () {
const duration = this.animationEnd - this.animationStart;
if (duration !== 0) {
@ -1084,13 +1068,13 @@ export class TrackEntry {
}
/** Returns true if this track entry has been applied at least once.
* <p>
*
* See {@link AnimationState#apply(Skeleton)}. */
wasApplied () {
return this.nextTrackLast !== -1;
}
/** Returns true if there is a {@link #getNext()} track entry and it will become the current track entry during the next
/** Returns true if there is a {@link #next()} track entry and it will become the current track entry during the next
* {@link AnimationState#update(float)}. */
isNextReady () {
return this.next != null && this.nextTrackLast - this.next.delay >= 0;
@ -1214,11 +1198,11 @@ export enum EventType {
/** The interface to implement for receiving TrackEntry events. It is always safe to call AnimationState methods when receiving
* events.
* <p>
*
* TrackEntry events are collected during {@link AnimationState#update} and {@link AnimationState#apply} and
* fired only after those methods are finished.
* <p>
* See TrackEntry {@link TrackEntry#listener} and AnimationState
*
* 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
@ -1232,14 +1216,21 @@ export interface AnimationStateListener {
* mixing. */
interrupt?: (entry: TrackEntry) => void;
/** Invoked when this entry is no longer the current entry and will never be applied again. */
/** 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(TrackEntry)} 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. */
/** Invoked every time this entry's animation completes a loop. This may occur during mixing (after
* {@link #interrupt(TrackEntry)}).
*
* 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(Skeleton)}, 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. */
@ -1266,40 +1257,10 @@ export abstract class AnimationStateAdapter implements AnimationStateListener {
}
}
/** 1. A previously applied timeline has set this property.
*
* Result: Mix from the current pose to the timeline pose. */
export 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 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 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 const HOLD = 2;
export 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 const HOLD_MIX = 4;
export const SETUP = 1;
export const CURRENT = 2;

View File

@ -32,7 +32,7 @@ import type { SkeletonData } from "./SkeletonData.js";
import type { StringMap } from "./Utils.js";
/** Stores mix (crossfade) durations to be applied when {@link AnimationState} animations are changed. */
/** Stores mix (crossfade) durations to be applied when {@link AnimationState} animations are changed on the same track. */
export class AnimationStateData {
/** The SkeletonData to look up animations when they are specified by name. */
skeletonData: SkeletonData;
@ -78,8 +78,8 @@ export class AnimationStateData {
this.animationToMixTime[key] = duration;
}
/** Returns the mix duration to use when changing from the specified animation to the other, or the {@link #defaultMix} if
* no mix duration has been set. */
/** Returns the mix duration to use when changing from the specified animation to the other on the same track, or the
* {@link #defaultMix} if no mix duration has been set. */
getMix (from: Animation, to: Animation) {
const key = `${from.name}.${to.name}`;
const value = this.animationToMixTime[key];

View File

@ -51,12 +51,16 @@ export class AtlasAttachmentLoader implements AttachmentLoader {
this.allowMissingRegions = allowMissingRegions;
}
/** Sets each {@link Sequence#regions} by calling {@link #findRegion(String, String)} for each texture region using
* {@link Sequence#getPath(String, int)}. */
protected findRegions (name: string, basePath: string, sequence: Sequence) {
const regions = sequence.regions;
for (let i = 0, n = regions.length; i < n; i++)
regions[i] = this.findRegion(name, sequence.getPath(basePath, i));
}
/** Looks for the region with the specified path. If not found and {@link #allowMissingRegions} is false, an error is
* raised. */
protected findRegion (name: string, path: string) {
const region = this.atlas.findRegion(path);
if (!region && !this.allowMissingRegions)

View File

@ -28,16 +28,20 @@
*****************************************************************************/
import type { BoneData } from "./BoneData.js";
import type { BoneLocal } from "./BoneLocal.js";
import { BonePose } from "./BonePose.js";
import { PosedActive } from "./PosedActive.js";
/** The current pose for a bone, before constraints are applied.
*
* A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
* local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
* constraint or application code modifies the world transform after it was computed from the local transform. */
export class Bone extends PosedActive<BoneData, BoneLocal, BonePose> {
/** A node in a skeleton's hierarchy with a transform that affects its children and their attachments. A bone has a number of
* poses:
* <ul>
* <li>{@link #data}: The setup pose.
* <li>{@link #pose}: The unconstrained local pose. Set by animations and application code.
* <li>{@link #appliedPose}: The local pose to use for rendering. Possibly modified by constraints.
* <li>World transform: the local pose combined with the parent world transform. Computed on a pose by
* {@link BonePose#updateWorldTransform(Skeleton)} and {@link Skeleton#updateWorldTransform(Physics)}.
* </ul>
*/
export class Bone extends PosedActive<BoneData, BonePose> {
/** The parent bone, or null if this is the root bone. */
parent: Bone | null = null;
@ -49,11 +53,11 @@ export class Bone extends PosedActive<BoneData, BoneLocal, BonePose> {
constructor (data: BoneData, parent: Bone | null) {
super(data, new BonePose(), new BonePose());
this.parent = parent;
this.applied.bone = this;
this.constrained.bone = this;
this.appliedPose.bone = this;
this.constrainedPose.bone = this;
}
/** Make a copy of the bone. Does not copy the {@link #getChildren()} bones. */
/** Copy constructor. Does not copy the {@link #children} bones. */
copy (parent: Bone | null): Bone {
const copy = new Bone(this.data, parent);
copy.pose.set(this.pose);

View File

@ -27,17 +27,17 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { BoneLocal } from "./BoneLocal.js";
import { BonePose } from "./BonePose.js";
import { PosedData } from "./PosedData.js";
import type { Skeleton } from "./Skeleton.js";
import { Color } from "./Utils.js";
/** The setup pose for a bone. */
export class BoneData extends PosedData<BoneLocal> {
/** The index of the bone in {@link Skeleton.getBones}. */
export class BoneData extends PosedData<BonePose> {
/** The index of the bone in {@link Skeleton.bones}. */
index: number = 0;
/** @returns May be null. */
/** The parent bone, or null if this bone is the root. */
parent: BoneData | null = null;
/** The bone's length. */
@ -48,14 +48,14 @@ export class BoneData extends PosedData<BoneLocal> {
* rendered at runtime. */
readonly color = new Color();
/** The bone icon as it was in Spine, or null if nonessential data was not exported. */
/** The bone icon name as it was in Spine, or null if nonessential data was not exported. */
icon?: string;
/** False if the bone was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
visible = false;
constructor (index: number, name: string, parent: BoneData | null) {
super(name, new BoneLocal());
super(name, new BonePose());
if (index < 0) throw new Error("index must be >= 0.");
if (!name) throw new Error("name cannot be null.");
this.index = index;
@ -65,7 +65,7 @@ export class BoneData extends PosedData<BoneLocal> {
copy (parent: BoneData | null): BoneData {
const copy = new BoneData(this.index, this.name, parent);
copy.length = this.length;
copy.setup.set(this.setup);
copy.setupPose.set(this.setupPose);
return copy;
}
}

View File

@ -29,27 +29,57 @@
import type { Bone } from "./Bone.js";
import { Inherit } from "./BoneData.js";
import { BoneLocal } from "./BoneLocal.js";
import type { Physics } from "./Physics.js";
import type { Pose } from "./Pose.js";
import type { Skeleton } from "./Skeleton.js";
import type { Update } from "./Update.js";
import { MathUtils, type Vector2 } from "./Utils.js";
/** The applied pose for a bone. This is the {@link Bone} pose with constraints applied and the world transform computed by
* {@link Skeleton#updateWorldTransform()}. */
export class BonePose extends BoneLocal implements Update {
/** The applied local pose and world transform for a bone. This is the {@link Bone#getPose()} with constraints applied and the
* world transform computed by {@link Skeleton#updateWorldTransform(Physics)} and {@link #updateWorldTransform(Skeleton)}.
* <p>
* If the world transform is changed, call {@link #updateLocalTransform(Skeleton)} before using the local transform. The local
* transform may be needed by other code (eg to apply another constraint).
* <p>
* After changing the world transform, call {@link #updateWorldTransform(Skeleton)} on every descendant bone. It may be more
* convenient to modify the local transform instead, then call {@link Skeleton#updateWorldTransform(Physics)} to update the world
* transforms for all bones and apply constraints. */
export class BonePose implements Pose<BonePose>, Update {
bone!: Bone;
/** Part of the world transform matrix for the X axis. If changed, {@link updateLocalTransform()} should be called. */
/** The local x translation. */
x = 0;
/** The local y translation. */
y = 0;
/** The local rotation in degrees, counter clockwise. */
rotation = 0;
/** The local scaleX. */
scaleX = 0;
/** The local scaleY. */
scaleY = 0;
/** The local shearX. */
shearX = 0;
/** The local shearY. */
shearY = 0;
inherit = Inherit.Normal;
/** The world transform <code>[a b][c d]</code> x-axis x component. */
a = 0;
/** Part of the world transform matrix for the Y axis. If changed, {@link updateLocalTransform()} should be called. */
/** The world transform <code>[a b][c d]</code> y-axis x component. */
b = 0;
/** Part of the world transform matrix for the X axis. If changed, {@link updateLocalTransform()} should be called. */
/** The world transform <code>[a b][c d]</code> x-axis y component. */
c = 0;
/** Part of the world transform matrix for the Y axis. If changed, {@link updateLocalTransform()} should be called. */
/** The world transform <code>[a b][c d]</code> y-axis y component. */
d = 0;
/** The world X position. If changed, {@link updateLocalTransform()} should be called. */
@ -61,13 +91,48 @@ export class BonePose extends BoneLocal implements Update {
world = 0;
local = 0;
set (pose: BonePose): void {
if (pose == null) throw new Error("pose cannot be null.");
this.x = pose.x;
this.y = pose.y;
this.rotation = pose.rotation;
this.scaleX = pose.scaleX;
this.scaleY = pose.scaleY;
this.shearX = pose.shearX;
this.shearY = pose.shearY;
this.inherit = pose.inherit;
}
setPosition (x: number, y: number): void {
this.x = x;
this.y = y;
}
setScale (scaleX: number, scaleY: number): void;
setScale (scale: number): void;
setScale (scaleOrX: number, scaleY?: number): void {
this.scaleX = scaleOrX;
this.scaleY = scaleY === undefined ? scaleOrX : scaleY;
}
/** Determines how parent world transforms affect this bone. */
public getInherit (): Inherit {
return this.inherit;
}
public setInherit (inherit: Inherit): void {
if (inherit == null) throw new Error("inherit cannot be null.");
this.inherit = inherit;
}
/** Called by {@link Skeleton#updateCache()} to compute the world transform, if needed. */
public update (skeleton: Skeleton, physics: Physics): void {
if (this.world !== skeleton._update) this.updateWorldTransform(skeleton);
}
/** Computes the world transform using the parent bone's applied pose and this pose. Child bones are not updated.
* <p>
/** Computes the world transform using the parent bone's world transform and this applied local pose. Child bones are not
* updated.
*
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide. */
updateWorldTransform (skeleton: Skeleton): void {
@ -94,7 +159,7 @@ export class BonePose extends BoneLocal implements Update {
return;
}
const parent = this.bone.parent.applied;
const parent = this.bone.parent.appliedPose;
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
this.worldX = pa * this.x + pb * this.y + parent.worldX;
this.worldY = pc * this.x + pd * this.y + parent.worldY;
@ -207,7 +272,7 @@ export class BonePose extends BoneLocal implements Update {
return;
}
const parent = this.bone.parent.applied;
const parent = this.bone.parent.appliedPose;
let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
let pid = 1 / (pa * pd - pb * pc);
let ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
@ -274,8 +339,9 @@ export class BonePose extends BoneLocal implements Update {
}
}
/** If the world transform has been modified and the local transform no longer matches, {@link #updateLocalTransform(Skeleton)}
* is called. */
/** If the world transform has been modified by constraints and the local transform no longer matches,
* {@link #updateLocalTransform(Skeleton)} is called. Call this after {@link Skeleton#updateWorldTransform(Physics)} before
* using the applied local transform. */
public validateLocalTransform (skeleton: Skeleton): void {
if (this.local === skeleton._update) this.updateLocalTransform(skeleton);
}
@ -295,7 +361,7 @@ export class BonePose extends BoneLocal implements Update {
resetWorld (update: number): void {
const children = this.bone.children;
for (let i = 0, n = children.length; i < n; i++) {
const child = children[i].applied;
const child = children[i].appliedPose;
if (child.world === update) {
child.world = 0;
child.local = 0;
@ -304,7 +370,8 @@ export class BonePose extends BoneLocal implements Update {
}
}
/** The world rotation for the X axis, calculated using {@link a} and {@link c}. */
/** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. This is the direction the bone is
* pointing. */
public getWorldRotationX (): number {
return MathUtils.atan2Deg(this.c, this.a);
}
@ -361,13 +428,13 @@ export class BonePose extends BoneLocal implements Update {
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public worldToParent (world: Vector2): Vector2 {
if (world == null) throw new Error("world cannot be null.");
return this.bone.parent == null ? world : this.bone.parent.applied.worldToLocal(world);
return this.bone.parent == null ? world : this.bone.parent.appliedPose.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public parentToWorld (world: Vector2): Vector2 {
if (world == null) throw new Error("world cannot be null.");
return this.bone.parent == null ? world : this.bone.parent.applied.localToWorld(world);
return this.bone.parent == null ? world : this.bone.parent.appliedPose.localToWorld(world);
}
/** Transforms a world rotation to a local rotation. */
@ -384,10 +451,7 @@ export class BonePose extends BoneLocal implements Update {
return MathUtils.atan2Deg(cos * this.c + sin * this.d, cos * this.a + sin * this.b);
}
/** Rotates the world transform the specified amount.
* <p>
* After changes are made to the world transform, {@link updateLocalTransform} should be called on this bone and any
* child bones, recursively. */
/** Rotates the world transform the specified amount. */
rotateWorld (degrees: number) {
degrees *= MathUtils.degRad;
const sin = Math.sin(degrees), cos = Math.cos(degrees);

View File

@ -38,7 +38,7 @@ export abstract class Constraint<
T extends Constraint<T, D, P>,
D extends ConstraintData<T, P>,
P extends Pose<P>>
extends PosedActive<D, P, P> implements Update {
extends PosedActive<D, P> implements Update {
constructor (data: D, pose: P, constrained: P) {
super(data, pose, constrained);

View File

@ -27,66 +27,47 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { Inherit } from "./BoneData.js";
import type { Pose } from "./Pose.js"
import type { Slot } from "./Slot";
import { Utils } from "./Utils";
/** Stores a bone's local pose. */
export class BoneLocal implements Pose<BoneLocal> {
/** Stores the skeleton's draw order, which is the order that each slot's attachment is rendered. */
export class DrawOrder {
readonly setupPose: Slot[];
/** The local x translation. */
x = 0;
/** The unconstrained draw order, set by animations and application code. */
readonly pose: Slot[];
readonly constrainedPose: Slot[];
/** The local y translation. */
y = 0;
/** The constrained draw order for rendering. If no constraints modify the draw order, this is the same as {@link #pose}.
* Otherwise it is a copy of {@link #pose} modified by constraints. */
appliedPose: Slot[];
/** The local rotation in degrees, counter clockwise. */
rotation = 0;
/** The local scaleX. */
scaleX = 0;
/** The local scaleY. */
scaleY = 0;
/** The local shearX. */
shearX = 0;
/** The local shearY. */
shearY = 0;
inherit = Inherit.Normal;
set (pose: BoneLocal): void {
if (pose == null) throw new Error("pose cannot be null.");
this.x = pose.x;
this.y = pose.y;
this.rotation = pose.rotation;
this.scaleX = pose.scaleX;
this.scaleY = pose.scaleY;
this.shearX = pose.shearX;
this.shearY = pose.shearY;
this.inherit = pose.inherit;
constructor (setupPose: Slot[]) {
this.setupPose = setupPose;
this.pose = [...setupPose];
this.constrainedPose = [];
this.appliedPose = this.pose;
}
setPosition (x: number, y: number): void {
this.x = x;
this.y = y;
/** Sets the unconstrained draw order to the setup pose order. */
useSetupPose () {
this.pose.length = this.setupPose.length;
Utils.arrayCopy(this.setupPose, 0, this.pose, 0, this.setupPose.length);
}
setScale (scaleX: number, scaleY: number): void;
setScale (scale: number): void;
setScale (scaleOrX: number, scaleY?: number): void {
this.scaleX = scaleOrX;
this.scaleY = scaleY === undefined ? scaleOrX : scaleY;
/** Sets the applied pose to the unconstrained pose, for when no constraints will modify the draw order. */
usePose () {
this.appliedPose = this.pose;
}
/** Determines how parent world transforms affect this bone. */
public getInherit (): Inherit {
return this.inherit;
/** Sets the applied pose to the constrained pose, in anticipation of the applied pose being modified by constraints. */
useConstrained () {
this.appliedPose = this.constrainedPose;
}
public setInherit (inherit: Inherit): void {
if (inherit == null) throw new Error("inherit cannot be null.");
this.inherit = inherit;
/** Copies the unconstrained pose to the constrained pose, as a starting point for constraints to be applied. */
resetConstrained () {
this.constrainedPose.length = this.pose.length;
Utils.arrayCopy(this.pose, 0, this.constrainedPose, 0, this.pose.length);
}
}

View File

@ -27,23 +27,35 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import type { EventData } from "./EventData.js";
import type { Timeline } from "./Animation.js";
import type { AnimationStateListener } from "./AnimationState.js";
import type { EventData } from "./EventData.js";
/** Stores the current pose values for an {@link Event}.
/** Fired by {@link EventTimeline} when specific animation times are reached.
*
* See Timeline {@link Timeline.apply()},
* AnimationStateListener {@link AnimationStateListener.event()}, and
* [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */
export class Event {
readonly data: EventData;
intValue: number = 0;
floatValue: number = 0;
stringValue: string | null = null;
/** The animation time this event was keyed, or -1 for the setup pose. */
time: number = 0;
readonly data: EventData;
/** The integer payload for this event. */
intValue: number = 0;
/** The float payload for this event. */
floatValue: number = 0;
stringValue: string | null = null;
/** If an audio path is set, the volume for the audio. */
volume: number = 0;
/** If an audio path is set, the left/right balance for the audio. */
balance: number = 0;
constructor (time: number, data: EventData) {

View File

@ -27,17 +27,31 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { Event } from "./Event.js";
/** Stores the setup pose values for an {@link Event}.
*
* See [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */
export class EventData {
/** The name of the event, unique across all events in the skeleton.
*
* See {@link SkeletonData#findEvent(String)}. */
name: string;
intValue: number = 0;
floatValue: number = 0;
stringValue: string | null = null;
audioPath: string | null = null;
volume: number = 0;
balance: number = 0;
_audioPath: string | null = null;
/** Path to an audio file relative to the audio folder as defined in Spine. */
get audioPath (): string {
// biome-ignore lint/style/noNonNullAssertion: can't be null after initialization
return this._audioPath!;
}
set audioPath (audioPath: string | null) {
if (audioPath == null) throw new Error("audioPath cannot be null.");
this._audioPath = audioPath;
}
/** The setup values that are shared by all events with this data. */
readonly setupPose = new Event(-1, this);
constructor (name: string) {
this.name = name;

View File

@ -37,8 +37,8 @@ import type { Physics } from "./Physics.js";
import type { Skeleton } from "./Skeleton.js";
import { MathUtils } from "./Utils.js";
/** Stores the current pose for an IK constraint. An IK constraint adjusts the rotation of 1 or 2 constrained bones so the tip of
* the last bone is as close to the target bone as possible.
/** Adjusts the local rotation of 1 or 2 constrained bones so the world position of the tip of the last bone is as close to the
* target bone as possible.
*
* See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */
export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkConstraintPose> {
@ -54,7 +54,7 @@ export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkC
this.bones = [] as BonePose[];
for (const boneData of data.bones)
this.bones.push(skeleton.bones[boneData.index].constrained);
this.bones.push(skeleton.bones[boneData.index].constrainedPose);
this.target = skeleton.bones[data.target.index];
}
@ -66,9 +66,9 @@ export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkC
}
update (skeleton: Skeleton, physics: Physics) {
const p = this.applied;
const p = this.appliedPose;
if (p.mix === 0) return;
const target = this.target.applied;
const target = this.target.appliedPose;
const bones = this.bones;
switch (bones.length) {
case 1:
@ -117,7 +117,7 @@ export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkC
bone.modifyLocal(skeleton);
// biome-ignore lint/style/noNonNullAssertion: reference runtime
const p = bone.bone.parent!.applied;
const p = bone.bone.parent!.appliedPose;
let pa = p.a, pb = p.b, pc = p.c, pd = p.d;
let rotationIK = -bone.shearX - bone.rotation, tx = 0, ty = 0;
@ -211,7 +211,7 @@ export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkC
cwy = c * child.x + d * child.y + parent.worldY;
}
// biome-ignore lint/style/noNonNullAssertion: reference runtime
const pp = parent.bone.parent!.applied;
const pp = parent.bone.parent!.appliedPose;
a = pp.a;
b = pp.b;
c = pp.c;

View File

@ -48,8 +48,8 @@ export class IkConstraintData extends ConstraintData<IkConstraint, IkConstraintP
else return this._target;
}
/** When true and {@link IkConstraintPose.compress} or {@link IkConstraintPose.stretch} is used, the bone is scaled
* on both the X and Y axes. */
/** When true and {@link IkConstraintPose#compress} or {@link IkConstraintPose#stretch} is used, the bone is scaled on both the
* X and Y axes. */
uniform = false;
constructor (name: string) {

View File

@ -29,7 +29,7 @@
import type { Pose } from "./Pose";
/** Stores the current pose for an IK constraint. */
/** Stores a pose for an IK constraint. */
export class IkConstraintPose implements Pose<IkConstraintPose> {
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
bendDirection = 0;
@ -38,9 +38,9 @@ export class IkConstraintPose implements Pose<IkConstraintPose> {
compress = false;
/** When true and the target is out of range, the parent bone is scaled to reach it.
*
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link softness} is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
* <p>
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if {@link #softness} is > 0,
* and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
stretch = false;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.

View File

@ -41,8 +41,7 @@ import type { Slot } from "./Slot.js";
import { MathUtils, Utils } from "./Utils.js";
/** Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the
* constrained bones so they follow a {@link PathAttachment}.
/** Adjusts the rotation, translation, and scale of the constrained bones so they follow a {@link PathAttachment}.
*
* See [Path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */
export class PathConstraint extends Constraint<PathConstraint, PathConstraintData, PathConstraintPose> {
@ -69,7 +68,7 @@ export class PathConstraint extends Constraint<PathConstraint, PathConstraintDat
this.bones = [] as BonePose[];
for (const boneData of this.data.bones)
this.bones.push(skeleton.bones[boneData.index].constrained);
this.bones.push(skeleton.bones[boneData.index].constrainedPose);
this.slot = skeleton.slots[data.slot.index];
}
@ -81,10 +80,10 @@ export class PathConstraint extends Constraint<PathConstraint, PathConstraintDat
}
update (skeleton: Skeleton, physics: Physics) {
const attachment = this.slot.applied.attachment;
const attachment = this.slot.appliedPose.attachment;
if (!(attachment instanceof PathAttachment)) return;
const p = this.applied;
const p = this.appliedPose;
const mixRotate = p.mixRotate, mixX = p.mixX, mixY = p.mixY;
if (mixRotate === 0 && mixX === 0 && mixY === 0) return;
@ -156,7 +155,7 @@ export class PathConstraint extends Constraint<PathConstraint, PathConstraintDat
tip = data.rotateMode === RotateMode.Chain;
else {
tip = false;
const bone = this.slot.bone.applied;
const bone = this.slot.bone.appliedPose;
offsetRotation *= bone.a * bone.d - bone.b * bone.c > 0 ? MathUtils.degRad : -MathUtils.degRad;
}
for (let i = 0, ip = 3, u = skeleton._update; i < boneCount; i++, ip += 3) {
@ -210,7 +209,7 @@ export class PathConstraint extends Constraint<PathConstraint, PathConstraintDat
computeWorldPositions (skeleton: Skeleton, path: PathAttachment, spacesCount: number, tangents: boolean) {
const slot = this.slot;
let position = this.applied.position;
let position = this.appliedPose.position;
let spaces = this.spaces, out = Utils.setArraySize(this.positions, spacesCount * 3 + 2), world: Array<number> = this.world;
const closed = path.closed;
let verticesLength = path.worldVerticesLength, curveCount = verticesLength / 6, prevCurve = PathConstraint.NONE;

View File

@ -32,7 +32,7 @@ export enum Physics {
/** Physics are not updated or applied. */
none,
/** Physics are reset to the current pose. */
/** Physics are {@link PhysicsConstraint#reset() reset}. */
reset,
/** Physics are updated and the pose from physics is applied. */

View File

@ -36,7 +36,7 @@ import { Skeleton } from "./Skeleton.js";
import { MathUtils } from "./Utils.js";
/** Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
/** Applies physics to a bone.
* <p>
* See <a href="http://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsConstraintData, PhysicsConstraintPose> {
@ -68,7 +68,7 @@ export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsCons
super(data, new PhysicsConstraintPose(), new PhysicsConstraintPose());
if (skeleton == null) throw new Error("skeleton cannot be null.");
this.bone = skeleton.bones[data.bone.index].constrained;
this.bone = skeleton.bones[data.bone.index].constrainedPose;
}
public copy (skeleton: Skeleton) {
@ -77,6 +77,8 @@ export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsCons
return copy;
}
/** Resets all physics state that was the result of previous movement. Use this after moving a bone to prevent physics from
* reacting to the movement. */
reset (skeleton: Skeleton) {
this.remaining = 0;
this.lastTime = skeleton.time;
@ -95,7 +97,7 @@ export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsCons
this.scaleVelocity = 0;
}
/** Translates the physics constraint so next {@link update} forces are applied as if the bone moved an
/** Translates the physics constraint so the next {@link #update(Skeleton, Physics)} forces are applied as if the bone moved an
* additional amount in world space. */
translate (x: number, y: number) {
this.ux -= x;
@ -104,8 +106,8 @@ export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsCons
this.cy -= y;
}
/** Rotates the physics constraint so next {@link update} forces are applied as if the bone rotated around the
* specified point in world space. */
/** Rotates the physics constraint so the next {@link #update(Skeleton, Physics)} forces are applied as if the bone rotated
* around the specified point in world space. */
rotate (x: number, y: number, degrees: number) {
const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
const dx = this.cx - x, dy = this.cy - y;
@ -114,7 +116,7 @@ export class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsCons
/** Applies the constraint to the constrained bones. */
update (skeleton: Skeleton, physics: Physics) {
const p = this.applied;
const p = this.appliedPose;
const mix = p.mix;
if (mix === 0) return;

View File

@ -46,19 +46,46 @@ export class PhysicsConstraintData extends ConstraintData<PhysicsConstraint, Phy
}
private _bone: BoneData | null = null;
/** Physics influence on x translation, 0-1. */
x = 0;
/** Physics influence on y translation, 0-1. */
y = 0;
/** Physics influence on rotation, 0-1. */
rotate = 0;
/** Physics influence on scaleX, 0-1. */
scaleX = 0;
/** Physics influence on shearX, 0-1. */
shearX = 0;
/** Movement greater than the limit will not have a greater affect on physics. */
limit = 0;
/** The time in milliseconds required to advanced the physics simulation one step. */
step = 0;
/** True when this constraint's inertia is controlled by global slider timelines. */
inertiaGlobal = false;
/** True when this constraint's strength is controlled by global slider timelines. */
strengthGlobal = false;
/** True when this constraint's damping is controlled by global slider timelines. */
dampingGlobal = false;
/** True when this constraint's mass is controlled by global slider timelines. */
massGlobal = false;
/** True when this constraint's wind is controlled by global slider timelines. */
windGlobal = false;
/** True when this constraint's gravity is controlled by global slider timelines. */
gravityGlobal = false;
/** True when this constraint's mix is controlled by global slider timelines. */
mixGlobal = false;
constructor (name: string) {

View File

@ -31,13 +31,26 @@ import type { Pose } from "./Pose"
/** Stores a pose for a physics constraint. */
export class PhysicsConstraintPose implements Pose<PhysicsConstraintPose> {
/** Controls how much bone movement is converted into physics movement. */
inertia = 0;
/** The amount of force used to return properties to the unconstrained value. */
strength = 0;
/** Reduces the speed of physics movements, with more of a reduction at higher speeds. */
damping = 0;
/** Determines susceptibility to acceleration. */
massInverse = 0;
/** Applies a constant force along the {@link Skeleton#windX}, {@link Skeleton#windY} vector. */
wind = 0;
/** Applies a constant force along the {@link Skeleton#gravityX}, {@link Skeleton#gravityY} vector. */
gravity = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */
/** A percentage (0+) that controls the mix between the constrained and unconstrained poses. */
mix = 0;
public set (pose: PhysicsConstraintPose) {

View File

@ -27,6 +27,8 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
/** An interface for an object representing a pose. */
export interface Pose<P> {
/** Sets this pose to the specified pose. */
set (pose: P): void;
}

View File

@ -30,52 +30,65 @@
import type { Pose } from "./Pose.js";
import type { PosedData } from "./PosedData.js";
/** The base class for an object with a number of poses:
* <ul>
* <li>{@link #data}: The setup pose.
* <li>{@link #pose}: The unconstrained pose. Set by animations and application code.
* <li>{@link #appliedPose}: The pose to use for rendering. Possibly modified by constraints.
* </ul>
*/
export abstract class Posed<
D extends PosedData<P>,
P extends Pose<P>,
A extends P> {
P extends Pose<P>> {
/** The constraint's setup pose data. */
readonly data: D;
readonly pose: A;
readonly constrained: A;
applied: A;
readonly pose: P;
readonly constrainedPose: P;
appliedPose: P;
constructor (data: D, pose: A, constrained: A) {
constructor (data: D, pose: P, constrainedPose: P) {
if (data == null) throw new Error("data cannot be null.");
this.data = data;
this.pose = pose;
this.constrained = constrained;
this.applied = pose;
this.constrainedPose = constrainedPose;
this.appliedPose = pose;
}
/** Sets the unconstrained pose to the setup pose. */
public setupPose (): void {
this.pose.set(this.data.setup);
this.pose.set(this.data.setupPose);
}
/** The constraint's setup pose data. */
/** The setup pose data. May be shared with multiple instances. */
public getData (): D {
return this.data;
}
/** The unconstrained pose for this object, set by animations and application code. */
public getPose (): P {
return this.pose;
}
public getAppliedPose (): A {
return this.applied;
/** The pose to use for rendering. If no constraints modify this pose, this is the same as {@link #pose}. Otherwise it is a
* copy of {@link #pose} modified by constraints. */
public getAppliedPose (): P {
return this.appliedPose;
}
usePose () { // Port: usePose - reference runtime: pose()
this.applied = this.pose;
/** Sets the applied pose to the unconstrained pose, for when no constraints will modify the pose. */
usePose () {
this.appliedPose = this.pose;
}
useConstrained () { // Port: useConstrained - reference runtime: constrained()
this.applied = this.constrained;
/** Sets the applied pose to the constrained pose, in anticipation of the applied pose being modified by constraints. */
useConstrained () {
this.appliedPose = this.constrainedPose;
}
/** Sets the constrained pose to the unconstrained pose, as a starting point for constraints to be applied. */
resetConstrained () { // Port: resetConstrained - reference runtime: reset()
this.constrained.set(this.pose);
this.constrainedPose.set(this.pose);
}
}

View File

@ -33,26 +33,23 @@ import type { PosedData } from "./PosedData.js";
import type { Skeleton } from "./Skeleton";
/** A posed object that may be active or inactive. */
export abstract class PosedActive<
D extends PosedData<P>,
P extends Pose<P>,
A extends P>
extends Posed<D, P, A> {
P extends Pose<P>>
extends Posed<D, P> {
active = false;
constructor (data: D, pose: A, constrained: A) {
protected constructor (data: D, pose: P, constrained: P) {
super(data, pose, constrained);
this.setupPose();
}
/** Returns false when this constraint won't be updated by
* {@link Skeleton.updateWorldTransform()} because a skin is required and the
* {@link Skeleton.getSkin() active skin} does not contain this item.
* @see Skin.getBones()
* @see Skin.getConstraints()
* @see PosedData.getSkinRequired()
* @see Skeleton.updateCache() */
* {@link Skeleton#updateWorldTransform(com.esotericsoftware.spine.Physics)} because a skin is required and the
* {@link Skeleton#skin active skin} does not contain this item. See {@link Skin#bones}, {@link Skin#constraints},
* {@link PosedData#skinRequired}, and {@link Skeleton#updateCache()}. */
public isActive (): boolean {
return this.active;
}

View File

@ -29,23 +29,22 @@
import type { Pose } from "./Pose.js";
/** The base class for all constrained datas. */
/** The base class for storing setup data for a posed object. May be shared with multiple instances. */
export abstract class PosedData<P extends Pose<P>> {
/** The constraint's name, which is unique across all constraints in the skeleton of the same type. */
readonly name: string;
readonly setup: P;
readonly setupPose: P;
/** When true, {@link Skeleton.updateWorldTransform} only updates this constraint if the {@link Skeleton.skin}
/** When true, {@link Skeleton#updateWorldTransform(Physics)} only updates this constraint if the {@link Skeleton#skin}
* contains this constraint.
*
* See {@link Skin.constraints}. */
* <p>
* See {@link Skin#constraints}. */
skinRequired = false;
constructor (name: string, setup: P) {
constructor (name: string, setupPose: P) {
if (name == null) throw new Error("name cannot be null.");
this.name = name;
this.setup = setup;
this.setupPose = setupPose;
}
}

View File

@ -33,6 +33,7 @@ import { MeshAttachment } from "./attachments/MeshAttachment.js";
import { RegionAttachment } from "./attachments/RegionAttachment.js";
import { Bone } from "./Bone.js";
import type { Constraint } from "./Constraint.js";
import { DrawOrder } from "./DrawOrder.js";
import type { Physics } from "./Physics.js";
import { PhysicsConstraint } from "./PhysicsConstraint.js";
import type { Posed } from "./Posed.js";
@ -42,7 +43,10 @@ import type { Skin } from "./Skin.js";
import { Slot } from "./Slot.js";
import { Color, type NumberArrayLike, Utils, Vector2 } from "./Utils.js";
/** Stores the current pose for a skeleton.
/** Stores bones and slots to be posed by animations and application code. Multiple skeleton instances can share the same
* {@link SkeletonData}, including animations, attachments, and skins.
*
* After posing, call {@link #updateWorldTransform(Physics)} to apply constraints and compute world transforms for rendering.
*
* See [Instance objects](http://esotericsoftware.com/spine-runtime-architecture#Instance-objects) in the Spine Runtimes Guide. */
export class Skeleton {
@ -58,11 +62,12 @@ export class Skeleton {
/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
readonly bones: Array<Bone>;
/** The skeleton's slots. */
/** The skeleton's slots. To add a slot, also add it to {@link DrawOrder#pose}. */
readonly slots: Array<Slot>;
/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */
drawOrder: Array<Slot>;
/** The skeleton's draw order. Use {@link DrawOrder#appliedPose} for rendering and {@link DrawOrder#pose} for changing the draw
* order. */
readonly drawOrder: DrawOrder;
/** The skeleton's constraints. */
// biome-ignore lint/suspicious/noExplicitAny: reference runtime does not restrict to specific types
@ -76,7 +81,7 @@ export class Skeleton {
readonly _updateCache = [] as any[];
// biome-ignore lint/suspicious/noExplicitAny: reference runtime does not restrict to specific types
readonly resetCache: Array<Posed<any, any, any>> = [];
readonly resetCache: Array<Posed<any, any>> = [];
/** The skeleton's current skin. May be null. */
skin: Skin | null = null;
@ -112,14 +117,21 @@ export class Skeleton {
* Bones that do not inherit translation are still affected by this property. */
y = 0;
/** Returns the skeleton's time. This is used for time-based manipulations, such as {@link PhysicsConstraint}.
/** Returns the skeleton's time, is used for time-based manipulations, such as {@link PhysicsConstraint}.
*
* See {@link _update()}. */
time = 0;
/** The x component of a vector that defines the direction {@link PhysicsConstraintPose#wind} is applied. */
windX = 1;
/** The y component of a vector that defines the direction {@link PhysicsConstraintPose#wind} is applied. */
windY = 0;
/** The x component of a vector that defines the direction {@link PhysicsConstraintPose#gravity} is applied. */
gravityX = 0;
/** The y component of a vector that defines the direction {@link PhysicsConstraintPose#gravity} is applied. */
gravityY = 1;
_update = 0;
@ -143,12 +155,9 @@ export class Skeleton {
}
this.slots = [] as Slot[];
this.drawOrder = [] as Slot[];
for (const slotData of this.data.slots) {
const slot = new Slot(slotData, this);
this.slots.push(slot);
this.drawOrder.push(slot);
}
for (const slotData of this.data.slots)
this.slots.push(new Slot(slotData, this));
this.drawOrder = new DrawOrder(this.slots);
this.physics = [] as PhysicsConstraint[];
// biome-ignore lint/suspicious/noExplicitAny: reference runtime does not restrict to specific types
@ -164,12 +173,13 @@ export class Skeleton {
this.updateCache();
}
/** Caches information about bones and constraints. Must be called if the {@link getSkin()} is modified or if bones,
* constraints, or weighted path attachments are added or removed. */
/** Caches information about bones and constraints. Must be called if the {@link #skin} is modified or if bones, constraints,
* or weighted path attachments are added or removed. */
updateCache () {
this._updateCache.length = 0;
this.resetCache.length = 0;
this.drawOrder.usePose();
const slots = this.slots;
for (let i = 0, n = slots.length; i < n; i++)
slots[i].usePose();
@ -212,14 +222,14 @@ export class Skeleton {
n = this._updateCache.length;
for (let i = 0; i < n; i++) {
const updateable = this._updateCache[i];
if (updateable instanceof Bone) this._updateCache[i] = updateable.applied;
if (updateable instanceof Bone) this._updateCache[i] = updateable.appliedPose;
}
}
// biome-ignore lint/suspicious/noExplicitAny: reference runtime does not restrict to specific types
constrained (object: Posed<any, any, any>) {
if (object.pose === object.applied) {
constrained (object: Posed<any, any>) {
if (object.pose === object.appliedPose) {
object.useConstrained();
this.resetCache.push(object);
}
@ -250,6 +260,7 @@ export class Skeleton {
updateWorldTransform (physics: Physics): void {
this._update++;
this.drawOrder.resetConstrained();
const resetCache = this.resetCache;
for (let i = 0, n = this.resetCache.length; i < n; i++)
resetCache[i].resetConstrained();
@ -278,8 +289,8 @@ export class Skeleton {
/** Sets the slots and draw order to their setup pose values. */
setupPoseSlots () {
this.drawOrder.useSetupPose();
const slots = this.slots;
Utils.arrayCopy(slots, 0, this.drawOrder, 0, slots.length);
for (let i = 0, n = slots.length; i < n; i++)
slots[i].setupPose();
}
@ -315,14 +326,14 @@ export class Skeleton {
* See {@link setSkin()}. */
setSkin (skinName: string): void;
/** Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default skin}. If the
* skin is changed, {@link updateCache} is called.
/** Sets the skin used to look up attachments before looking in {@link SkeletonData#defaultSkin}. If the skin is changed,
* {@link #updateCache()} is called.
* <p>
* Attachments from the new skin are attached if the corresponding attachment from the old skin was attached. If there was no
* old skin, each slot's setup mode attachment is attached from the new skin.
* <p>
* After changing the skin, the visible attachments can be reset to those attached in the setup pose by calling
* {@link setupPoseSlots()}. Also, often {@link AnimationState.apply(Skeleton)} is called before the next time the skeleton is
* {@link #setupPoseSlots()}. Also, often {@link AnimationState#apply(Skeleton)} is called before the next time the skeleton is
* rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. */
setSkin (newSkin: Skin | null): void;
@ -364,18 +375,18 @@ export class Skeleton {
* name.
*
* See {@link getAttachment(number, string)}. */
getAttachment (slotName: string, attachmentName: string): Attachment | null;
getAttachment (slotName: string, placeholderName: string): Attachment | null;
/** Finds an attachment by looking in the {@link skin} and {@link SkeletonData.defaultSkin} using the slot index and
* attachment name. First the skin is checked and if the attachment was not found, the default skin is checked.
*
* See <a href="https://esotericsoftware.com/spine-runtime-skins">Runtime skins</a> in the Spine Runtimes Guide. */
getAttachment (slotIndex: number, attachmentName: string): Attachment | null;
getAttachment (slotIndex: number, placeholderName: string): Attachment | null;
getAttachment (slotNameOrIndex: string | number, attachmentName: string): Attachment | null {
getAttachment (slotNameOrIndex: string | number, placeholderName: string): Attachment | null {
if (typeof slotNameOrIndex === 'string')
return this.getAttachmentByName(slotNameOrIndex, attachmentName);
return this.getAttachmentByIndex(slotNameOrIndex, attachmentName);
return this.getAttachmentByName(slotNameOrIndex, placeholderName);
return this.getAttachmentByIndex(slotNameOrIndex, placeholderName);
}
/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment
@ -383,10 +394,10 @@ export class Skeleton {
*
* See {@link #getAttachment()}.
* @returns May be null. */
private getAttachmentByName (slotName: string, attachmentName: string): Attachment | null {
private getAttachmentByName (slotName: string, placeholderName: string): Attachment | null {
const slot = this.data.findSlot(slotName);
if (!slot) throw new Error(`Can't find slot with name ${slotName}`);
return this.getAttachment(slot.index, attachmentName);
return this.getAttachment(slot.index, placeholderName);
}
/** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot index and
@ -420,6 +431,8 @@ export class Skeleton {
slot.pose.setAttachment(attachment);
}
/** Finds a constraint of the specified type by comparing each constraints's name. It is more efficient to cache the results of
* this method than to call it multiple times. */
// biome-ignore lint/suspicious/noExplicitAny: reference runtime does not restrict to specific types
findConstraint<T extends Constraint<any, any, any>> (constraintName: string, type: new () => T): T | null {
if (constraintName == null) throw new Error("constraintName cannot be null.");
@ -432,8 +445,10 @@ export class Skeleton {
return null;
}
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose as `{ x: number, y: number, width: number, height: number }`.
* Note that this method will create temporary objects which can add to garbage collection pressure. Use `getBounds()` if garbage collection is a concern. */
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the applied pose.
* @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
* @param size An output value, the width and height of the AABB.
* @param temp Working memory to temporarily store attachments' computed world vertices. */
getBoundsRect (clipper?: SkeletonClipping) {
const offset = new Vector2();
const size = new Vector2();
@ -441,7 +456,8 @@ export class Skeleton {
return { x: offset.x, y: offset.y, width: size.x, height: size.y };
}
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose.
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the applied pose. Optionally applies
* clipping.
* @param offset An output value, the distance from the skeleton origin to the bottom left corner of the AABB.
* @param size An output value, the width and height of the AABB.
* @param temp Working memory to temporarily store attachments' computed world vertices.
@ -449,20 +465,21 @@ export class Skeleton {
getBounds (offset: Vector2, size: Vector2, temp: Array<number> = new Array<number>(2), clipper: SkeletonClipping | null = null) {
if (!offset) throw new Error("offset cannot be null.");
if (!size) throw new Error("size cannot be null.");
const drawOrder = this.drawOrder;
const drawOrder = this.drawOrder.appliedPose;
const slots = drawOrder;
let minX = Number.POSITIVE_INFINITY, minY = Number.POSITIVE_INFINITY, maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY;
for (let i = 0, n = drawOrder.length; i < n; i++) {
const slot = drawOrder[i];
const slot = slots[i];
if (!slot.bone.active) continue;
let verticesLength = 0;
let vertices: NumberArrayLike | null = null;
let triangles: NumberArrayLike | null = null;
const attachment = slot.pose.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment) {
if (attachment instanceof RegionAttachment) {
verticesLength = 8;
vertices = Utils.setArraySize(temp, verticesLength, 0);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), vertices, 0, 2);
triangles = Skeleton.quadTriangles;
} else if (attachment instanceof MeshAttachment) {
verticesLength = attachment.worldVerticesLength;

View File

@ -107,7 +107,7 @@ export class SkeletonBinary {
if (!name) throw new Error("Bone name must not be null.");
const parent = i === 0 ? null : bones[input.readInt(true)];
const data = new BoneData(i, name, parent);
const setup = data.setup;
const setup = data.setupPose;
setup.rotation = input.readFloat();
setup.x = input.readFloat() * scale;
setup.y = input.readFloat() * scale;
@ -133,10 +133,10 @@ export class SkeletonBinary {
if (!slotName) throw new Error("Slot name must not be null.");
const boneData = bones[input.readInt(true)];
const data = new SlotData(i, slotName, boneData);
Color.rgba8888ToColor(data.setup.color, input.readInt32());
Color.rgba8888ToColor(data.setupPose.color, input.readInt32());
const darkColor = input.readInt32();
if (darkColor !== -1) Color.rgb888ToColor(data.setup.darkColor = new Color(), darkColor);
if (darkColor !== -1) Color.rgb888ToColor(data.setupPose.darkColor = new Color(), darkColor);
data.attachmentName = input.readStringRef();
data.blendMode = input.readInt(true);
@ -161,7 +161,7 @@ export class SkeletonBinary {
const flags = input.readByte();
data.skinRequired = (flags & 1) !== 0;
data.uniform = (flags & 2) !== 0;
const setup = data.setup;
const setup = data.setupPose;
setup.bendDirection = (flags & 4) !== 0 ? -1 : 1;
setup.compress = (flags & 8) !== 0;
setup.stretch = (flags & 16) !== 0;
@ -243,7 +243,7 @@ export class SkeletonBinary {
if ((flags & 16) !== 0) data.offsets[TransformConstraintData.SCALEY] = input.readFloat();
if ((flags & 32) !== 0) data.offsets[TransformConstraintData.SHEARY] = input.readFloat();
flags = input.readByte();
const setup = data.setup;
const setup = data.setupPose;
if ((flags & 1) !== 0) setup.mixRotate = input.readFloat();
if ((flags & 2) !== 0) setup.mixX = input.readFloat();
if ((flags & 4) !== 0) setup.mixY = input.readFloat();
@ -265,7 +265,7 @@ export class SkeletonBinary {
data.spacingMode = (flags >> 2) & 0b11;
data.rotateMode = (flags >> 4) & 0b11;
if ((flags & 128) !== 0) data.offsetRotation = input.readFloat();
const setup = data.setup;
const setup = data.setupPose;
setup.position = input.readFloat();
if (data.positionMode === PositionMode.Fixed) setup.position *= scale;
setup.spacing = input.readFloat();
@ -288,7 +288,7 @@ export class SkeletonBinary {
if ((flags & 32) !== 0) data.shearX = input.readFloat();
data.limit = ((flags & 64) !== 0 ? input.readFloat() : 5000) * scale;
data.step = 1 / input.readUnsignedByte();
const setup = data.setup;
const setup = data.setupPose;
setup.inertia = input.readFloat();
setup.strength = input.readFloat();
setup.damping = input.readFloat();
@ -313,8 +313,8 @@ export class SkeletonBinary {
data.skinRequired = (flags & 1) !== 0;
data.loop = (flags & 2) !== 0;
data.additive = (flags & 4) !== 0;
if ((flags & 8) !== 0) data.setup.time = input.readFloat();
if ((flags & 16) !== 0) data.setup.mix = (flags & 32) !== 0 ? input.readFloat() : 1;
if ((flags & 8) !== 0) data.setupPose.time = input.readFloat();
if ((flags & 16) !== 0) data.setupPose.mix = (flags & 32) !== 0 ? input.readFloat() : 1;
if ((flags & 64) !== 0) {
data.local = (flags & 128) !== 0;
data.bone = bones[input.readInt(true)];
@ -385,13 +385,14 @@ export class SkeletonBinary {
const eventName = input.readString();
if (!eventName) throw new Error("Event data name must not be null");
const data = new EventData(eventName);
data.intValue = input.readInt(false);
data.floatValue = input.readFloat();
data.stringValue = input.readString();
data.audioPath = input.readString();
const setup = data.setupPose;
setup.intValue = input.readInt(false);
setup.floatValue = input.readFloat();
setup.stringValue = input.readString();
data._audioPath = input.readString();
if (data.audioPath) {
data.volume = input.readFloat();
data.balance = input.readFloat();
setup.volume = input.readFloat();
setup.balance = input.readFloat();
}
skeletonData.events.push(data);
}
@ -1164,7 +1165,7 @@ export class SkeletonBinary {
event.intValue = input.readInt(false);
event.floatValue = input.readFloat();
event.stringValue = input.readString();
if (event.stringValue == null) event.stringValue = eventData.stringValue;
if (event.stringValue == null) event.stringValue = eventData.setupPose.stringValue;
if (event.data.audioPath) {
event.volume = input.readFloat();
event.balance = input.readFloat();

View File

@ -76,7 +76,7 @@ export class SkeletonBounds {
for (let i = 0; i < slotCount; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment instanceof BoundingBoxAttachment) {
boundingBoxes.push(attachment);

View File

@ -94,10 +94,10 @@ export class SkeletonData {
/** The dopesheet FPS in Spine. Available only when nonessential data was exported. */
fps = 30;
/** The path to the images directory as defined in Spine. Available only when nonessential data was exported. May be null. */
/** The path to the images folder as defined in Spine. Available only when nonessential data was exported. May be null. */
imagesPath: string | null = null;
/** The path to the audio directory as defined in Spine. Available only when nonessential data was exported. May be null. */
/** The path to the audio folder as defined in Spine. Available only when nonessential data was exported. May be null. */
audioPath: string | null = null;
/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
@ -170,6 +170,8 @@ export class SkeletonData {
// --- Constraints.
/** Finds a constraint of the specified type by comparing each constraints's name. It is more efficient to cache the results of
* this method than to call it multiple times. */
// biome-ignore lint/suspicious/noExplicitAny: reference runtime does not restrict to specific types
findConstraint<T extends ConstraintData<any, any>> (constraintName: string, type: new (name: string) => T): T | null {
if (!constraintName) throw new Error("constraintName cannot be null.");

View File

@ -96,7 +96,7 @@ export class SkeletonJson {
if (parentName) parent = skeletonData.findBone(parentName);
const data = new BoneData(skeletonData.bones.length, boneMap.name, parent);
data.length = getValue(boneMap, "length", 0) * scale;
const setup = data.setup;
const setup = data.setupPose;
setup.x = getValue(boneMap, "x", 0) * scale;
setup.y = getValue(boneMap, "y", 0) * scale;
setup.rotation = getValue(boneMap, "rotation", 0);
@ -125,10 +125,10 @@ export class SkeletonJson {
const data = new SlotData(skeletonData.slots.length, slotName, boneData);
const color: string = getValue(slotMap, "color", null);
if (color) data.setup.color.setFromString(color);
if (color) data.setupPose.color.setFromString(color);
const dark: string = getValue(slotMap, "dark", null);
if (dark) data.setup.darkColor = Color.fromString(dark);
if (dark) data.setupPose.darkColor = Color.fromString(dark);
data.attachmentName = getValue(slotMap, "attachment", null);
data.blendMode = Utils.enumValue(BlendMode, getValue(slotMap, "blend", "normal"));
@ -159,7 +159,7 @@ export class SkeletonJson {
data.target = target;
data.uniform = getValue(constraintMap, "uniform", false);
const setup = data.setup;
const setup = data.setupPose;
setup.mix = getValue(constraintMap, "mix", 1);
setup.softness = getValue(constraintMap, "softness", 0) * scale;
setup.bendDirection = getValue(constraintMap, "bendPositive", true) ? 1 : -1;
@ -250,7 +250,7 @@ export class SkeletonJson {
data.offsets[TransformConstraintData.SCALEY] = getValue(constraintMap, "scaleY", 0);
data.offsets[TransformConstraintData.SHEARY] = getValue(constraintMap, "shearY", 0);
const setup = data.setup;
const setup = data.setupPose;
if (rotate) setup.mixRotate = getValue(constraintMap, "mixRotate", 1);
if (x) setup.mixX = getValue(constraintMap, "mixX", 1);
if (y) setup.mixY = getValue(constraintMap, "mixY", setup.mixX);
@ -281,7 +281,7 @@ export class SkeletonJson {
data.spacingMode = Utils.enumValue(SpacingMode, getValue(constraintMap, "spacingMode", "Length"));
data.rotateMode = Utils.enumValue(RotateMode, getValue(constraintMap, "rotateMode", "Tangent"));
data.offsetRotation = getValue(constraintMap, "rotation", 0);
const setup = data.setup;
const setup = data.setupPose;
setup.position = getValue(constraintMap, "position", 0);
if (data.positionMode === PositionMode.Fixed) setup.position *= scale;
setup.spacing = getValue(constraintMap, "spacing", 0);
@ -309,7 +309,7 @@ export class SkeletonJson {
data.shearX = getValue(constraintMap, "shearX", 0);
data.limit = getValue(constraintMap, "limit", 5000) * scale;
data.step = 1 / getValue(constraintMap, "fps", 60);
const setup = data.setup;
const setup = data.setupPose;
setup.inertia = getValue(constraintMap, "inertia", 0.5);
setup.strength = getValue(constraintMap, "strength", 100);
setup.damping = getValue(constraintMap, "damping", 0.85);
@ -334,8 +334,8 @@ export class SkeletonJson {
data.additive = getValue(constraintMap, "additive", false);
data.loop = getValue(constraintMap, "loop", false);
data.setup.time = getValue(constraintMap, "time", 0);
data.setup.mix = getValue(constraintMap, "mix", 1);
data.setupPose.time = getValue(constraintMap, "time", 0);
data.setupPose.mix = getValue(constraintMap, "mix", 1);
const boneName: string = constraintMap.bone;
if (boneName) {
@ -449,13 +449,14 @@ export class SkeletonJson {
for (const eventName in root.events) {
const eventMap = root.events[eventName];
const data = new EventData(eventName);
data.intValue = getValue(eventMap, "int", 0);
data.floatValue = getValue(eventMap, "float", 0);
data.stringValue = getValue(eventMap, "string", "");
data.audioPath = getValue(eventMap, "audio", null);
const setup = data.setupPose;
setup.intValue = getValue(eventMap, "int", 0);
setup.floatValue = getValue(eventMap, "float", 0);
setup.stringValue = getValue(eventMap, "string", "");
data._audioPath = getValue(eventMap, "audio", null);
if (data.audioPath) {
data.volume = getValue(eventMap, "volume", 1);
data.balance = getValue(eventMap, "balance", 0);
setup.volume = getValue(eventMap, "volume", setup.volume);
setup.balance = getValue(eventMap, "balance", setup.balance);
}
skeletonData.events.push(data);
}
@ -1211,15 +1212,16 @@ export class SkeletonJson {
let frame = 0;
for (let i = 0; i < map.events.length; i++, frame++) {
const eventMap = map.events[i];
const eventData = skeletonData.findEvent(eventMap.name);
if (!eventData) throw new Error(`Event not found: ${eventMap.name}`);
const event = new Event(Utils.toSinglePrecision(getValue(eventMap, "time", 0)), eventData);
event.intValue = getValue(eventMap, "int", eventData.intValue);
event.floatValue = getValue(eventMap, "float", eventData.floatValue);
event.stringValue = getValue(eventMap, "string", eventData.stringValue);
const data = skeletonData.findEvent(eventMap.name);
if (!data) throw new Error(`Event not found: ${eventMap.name}`);
const setup = data.setupPose;
const event = new Event(Utils.toSinglePrecision(getValue(eventMap, "time", 0)), data);
event.intValue = getValue(eventMap, "int", setup.intValue);
event.floatValue = getValue(eventMap, "float", setup.floatValue);
event.stringValue = getValue(eventMap, "string", setup.stringValue);
if (event.data.audioPath) {
event.volume = getValue(eventMap, "volume", 1);
event.balance = getValue(eventMap, "balance", 0);
event.volume = getValue(eventMap, "volume", setup.volume);
event.balance = getValue(eventMap, "balance", setup.volume);
}
timeline.setFrame(frame, event);
}

View File

@ -46,16 +46,17 @@ export class SkeletonRendererCore {
const clipper = this.clipping;
const drawOrder = skeleton.drawOrder.appliedPose;
for (let i = 0; i < skeleton.slots.length; i++) {
const slot = skeleton.drawOrder[i];
const attachment = slot.applied.attachment;
const slot = drawOrder[i];
const attachment = slot.appliedPose.attachment;
if (!attachment) {
clipper.clipEnd(slot);
continue;
}
const pose = slot.applied;
const pose = slot.appliedPose;
const slotColor = pose.color;
const alpha = slotColor.a;
if ((alpha === 0 || !slot.bone.active) && !(attachment instanceof ClippingAttachment)) {

View File

@ -36,15 +36,30 @@ import { Color, type StringMap } from "./Utils.js";
/** Stores an entry in the skin consisting of the slot index, name, and attachment **/
export class SkinEntry {
constructor (public slotIndex: number = 0, public name: string, public attachment: Attachment) { }
/** The {@link Skeleton#slots} index. */
slotIndex: number = 0;
placeholderName: string;
/** The attachment for this skin entry. */
attachment: Attachment
constructor (slotIndex: number = 0, placeholderName: string, attachment: Attachment) {
this.slotIndex = slotIndex;
this.placeholderName = placeholderName;
this.attachment = attachment;
}
}
/** Stores attachments by slot index and attachment name.
/** Stores attachments by slot index and placeholder name. Multiple {@link Skeleton} instances can use the same skins.
*
* See SkeletonData {@link SkeletonData#defaultSkin}, Skeleton {@link Skeleton#skin}, and
* See {@link SkeletonData#defaultSkin}, {@link Skeleton#skin}, and
* [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide. */
export class Skin {
/** The skin's name, which is unique across all skins in the skeleton. */
/** The skin's name, unique across all skins in the skeleton.
* <p>
* See {@link SkeletonData#findSkin(String)}. */
name: string;
attachments = [] as StringMap<Attachment>[];
@ -61,12 +76,12 @@ export class Skin {
}
/** Adds an attachment to the skin for the specified slot index and name. */
setAttachment (slotIndex: number, name: string, attachment: Attachment) {
setAttachment (slotIndex: number, placeholderName: string, attachment: Attachment) {
if (!attachment) throw new Error("attachment cannot be null.");
const attachments = this.attachments;
if (slotIndex >= attachments.length) attachments.length = slotIndex + 1;
if (!attachments[slotIndex]) attachments[slotIndex] = {};
attachments[slotIndex][name] = attachment;
attachments[slotIndex][placeholderName] = attachment;
}
/** Adds all attachments, bones, and constraints from the specified skin to this skin. */
@ -98,7 +113,7 @@ export class Skin {
const attachments = skin.getAttachments();
for (let i = 0; i < attachments.length; i++) {
const attachment = attachments[i];
this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment);
this.setAttachment(attachment.slotIndex, attachment.placeholderName, attachment.attachment);
}
}
@ -135,24 +150,24 @@ export class Skin {
if (!attachment.attachment) continue;
if (attachment.attachment instanceof MeshAttachment) {
attachment.attachment = attachment.attachment.newLinkedMesh();
this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment);
this.setAttachment(attachment.slotIndex, attachment.placeholderName, attachment.attachment);
} else {
attachment.attachment = attachment.attachment.copy();
this.setAttachment(attachment.slotIndex, attachment.name, attachment.attachment);
this.setAttachment(attachment.slotIndex, attachment.placeholderName, attachment.attachment);
}
}
}
/** Returns the attachment for the specified slot index and name, or null. */
getAttachment (slotIndex: number, name: string): Attachment | null {
/** Returns the attachment for the specified slot index and placeholderName, or null. */
getAttachment (slotIndex: number, placeholderName: string): Attachment | null {
const dictionary = this.attachments[slotIndex];
return dictionary ? dictionary[name] : null;
return dictionary ? dictionary[placeholderName] : null;
}
/** Removes the attachment in the skin for the specified slot index and name, if any. */
removeAttachment (slotIndex: number, name: string) {
/** Removes the attachment in the skin for the specified slot index and placeholderName, if any. */
removeAttachment (slotIndex: number, placeholderName: string) {
const dictionary = this.attachments[slotIndex];
if (dictionary) delete dictionary[name];
if (dictionary) delete dictionary[placeholderName];
}
/** Returns all attachments in this skin. */
@ -196,10 +211,10 @@ export class Skin {
const slotAttachment = slot.pose.getAttachment();
if (slotAttachment && slotIndex < oldSkin.attachments.length) {
const dictionary = oldSkin.attachments[slotIndex];
for (const key in dictionary) {
const skinAttachment: Attachment = dictionary[key];
for (const placeholderName in dictionary) {
const skinAttachment: Attachment = dictionary[placeholderName];
if (slotAttachment === skinAttachment) {
const attachment = this.getAttachment(slotIndex, key);
const attachment = this.getAttachment(slotIndex, placeholderName);
if (attachment) slot.pose.setAttachment(attachment);
break;
}

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { isConstraintTimeline, isSlotTimeline, MixBlend, MixDirection, PhysicsConstraintTimeline } from "./Animation.js";
import { DrawOrderFolderTimeline, DrawOrderTimeline, isConstraintTimeline, isSlotTimeline, PhysicsConstraintTimeline } from "./Animation.js";
import type { Bone } from "./Bone.js";
import { Constraint } from "./Constraint.js";
import type { Physics } from "./Physics.js";
@ -35,12 +35,13 @@ import type { Skeleton } from "./Skeleton.js";
import type { SliderData } from "./SliderData.js";
import { SliderPose } from "./SliderPose.js";
/** Stores the setup pose for a {@link PhysicsConstraint}.
*
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
/** Applies an animation based on either the slider's {@link SliderPose#time} or a bone's transform property.
* <p>
* See <a href="https://esotericsoftware.com/spine-sliders">Sliders</a> in the Spine User Guide. */
export class Slider extends Constraint<Slider, SliderData, SliderPose> {
private static readonly offsets = [0, 0, 0, 0, 0, 0];
/** When set, the bone's transform property is used to set the slider's {@link SliderPose#time}. */
bone: Bone | null = null;
constructor (data: SliderData, skeleton: Skeleton) {
@ -57,15 +58,15 @@ export class Slider extends Constraint<Slider, SliderData, SliderPose> {
}
public update (skeleton: Skeleton, physics: Physics) {
const p = this.applied;
const p = this.appliedPose;
if (p.mix === 0) return;
const data = this.data, animation = data.animation, bone = this.bone;
if (bone !== null) {
if (!bone.active) return;
if (data.local) bone.applied.validateLocalTransform(skeleton);
if (data.local) bone.appliedPose.validateLocalTransform(skeleton);
p.time = data.offset
+ (data.property.value(skeleton, bone.applied, data.local, Slider.offsets) - data.property.offset) * data.scale;
+ (data.property.value(skeleton, bone.appliedPose, data.local, Slider.offsets) - data.property.offset) * data.scale;
if (data.loop)
p.time = animation.duration + (p.time % animation.duration);
else
@ -75,10 +76,9 @@ export class Slider extends Constraint<Slider, SliderData, SliderPose> {
const bones = skeleton.bones;
const indices = animation.bones;
for (let i = 0, n = animation.bones.length; i < n; i++)
bones[indices[i]].applied.modifyLocal(skeleton);
bones[indices[i]].appliedPose.modifyLocal(skeleton);
animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, data.additive ? MixBlend.add : MixBlend.replace,
MixDirection.in, true);
animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, false, data.additive, false, true);
}
sort (skeleton: Skeleton) {
@ -105,6 +105,8 @@ export class Slider extends Constraint<Slider, SliderData, SliderPose> {
const t = timelines[i];
if (isSlotTimeline(t))
skeleton.constrained(slots[t.slotIndex]);
else if (t instanceof DrawOrderTimeline || t instanceof DrawOrderFolderTimeline)
skeleton.drawOrder.useConstrained();
else if (t instanceof PhysicsConstraintTimeline) {
if (t.constraintIndex === -1) {
for (let ii = 0; ii < physicsCount; ii++)

View File

@ -39,13 +39,29 @@ import type { FromProperty } from "./TransformConstraintData.js";
*
* See <a href="https://esotericsoftware.com/spine-slider-constraints">Slider constraints</a> in the Spine User Guide. */
export class SliderData extends ConstraintData<Slider, SliderPose> {
/** The animation the slider will apply. */
animation!: Animation;
/** When true, the animation is applied by adding it to the current pose rather than overwriting it. */
additive = false;
/** When true, the animation repeats after its duration, otherwise the last frame is used. */
loop = false;
/** When set, the bone's transform property is used to set the slider's {@link SliderPose#time}. */
bone: BoneData | null = null;
/** When a bone is set, the specified transform property is used to set the slider's {@link SliderPose#time}. */
property!: FromProperty;
/** When a bone is set, this is the scale of the {@link #property} value in relation to the slider time. */
scale = 0;
/** When a bone is set, the offset is added to the property. */
offset = 0;
/** When true and a bone is set, the bone's local transform property is read instead of its world transform. */
local = false;
constructor (name: string) {

View File

@ -31,7 +31,11 @@ import type { Pose } from "./Pose.js";
/** Stores a pose for a slider. */
export class SliderPose implements Pose<SliderPose> {
/** The time in the {@link SliderData#animation} to apply the animation. */
time = 0;
/** A percentage that controls the mix between the constrained and unconstrained poses. */
mix = 0;
set (pose: SliderPose) {

View File

@ -34,10 +34,11 @@ import type { SlotData } from "./SlotData.js";
import { SlotPose } from "./SlotPose.js";
import { Color } from "./Utils.js";
/** Stores a slot's current pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store
* state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared
* across multiple skeletons. */
export class Slot extends Posed<SlotData, SlotPose, SlotPose> {
/** Organizes attachments for {@link Skeleton#drawOrder} purposes and provide a place to store state for an attachment.
* <p>
* State cannot be stored in an attachment itself because attachments are stateless and may be shared across multiple
* skeletons. */
export class Slot extends Posed<SlotData, SlotPose> {
readonly skeleton: Skeleton;
/** The bone this slot belongs to. */
@ -50,18 +51,29 @@ export class Slot extends Posed<SlotData, SlotPose, SlotPose> {
if (!skeleton) throw new Error("skeleton cannot be null.");
this.skeleton = skeleton;
this.bone = skeleton.bones[data.boneData.index];
if (data.setup.darkColor != null) {
if (data.setupPose.darkColor != null) {
this.pose.darkColor = new Color();
this.constrained.darkColor = new Color();
this.constrainedPose.darkColor = new Color();
}
this.setupPose();
}
/** Copy constructor. */
public copy (slot: Slot, bone: Bone, skeleton: Skeleton) {
const copy = new Slot(slot.data, this.skeleton);
if (this.data.setupPose.darkColor != null) {
copy.pose.darkColor = new Color();
copy.constrainedPose.darkColor = new Color();
}
copy.pose.set(slot.pose);
return copy;
}
setupPose () {
this.pose.color.setFromColor(this.data.setup.color);
this.pose.color.setFromColor(this.data.setupPose.color);
// biome-ignore lint/style/noNonNullAssertion: reference runtime
if (this.pose.darkColor) this.pose.darkColor.setFromColor(this.data.setup.darkColor!);
this.pose.sequenceIndex = this.data.setup.sequenceIndex;
if (this.pose.darkColor) this.pose.darkColor.setFromColor(this.data.setupPose.darkColor!);
this.pose.sequenceIndex = this.data.setupPose.sequenceIndex;
if (!this.data.attachmentName)
this.pose.setAttachment(null);
else {

View File

@ -34,7 +34,7 @@ import { SlotPose } from "./SlotPose.js";
/** Stores the setup pose for a {@link Slot}. */
export class SlotData extends PosedData<SlotPose> {
/** The index of the slot in {@link Skeleton.getSlots()}. */
/** The index of the slot in {@link Skeleton.slots}. */
index: number = 0;
/** The bone this slot belongs to. */

View File

@ -33,12 +33,10 @@ import type { Sequence } from "./attachments/Sequence.js";
import type { Pose } from "./Pose.js";
import { Color } from "./Utils.js";
/** Stores a slot's pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store state
* for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared across
* multiple skeletons. */
/** Stores a slot's pose. */
export class SlotPose implements Pose<SlotPose> {
/** The color used to tint the slot's attachment. If {@link darkColor} is set, this is used as the light color for two
* color tinting. */
/** The color used to tint the slot's attachment. If {@link #darkColor} is set, this is used as the light color for two color
* tinting. */
readonly color = new Color(1, 1, 1, 1);
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark

View File

@ -37,8 +37,7 @@ import { TransformConstraintPose } from "./TransformConstraintPose.js";
import { MathUtils } from "./Utils.js";
/** Stores the current pose for a transform constraint. A transform constraint adjusts the world transform of the constrained
* bones to match that of the source bone.
/** Adjusts the world transform of the constrained bones to match that of the source bone.
*
* See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */
export class TransformConstraint extends Constraint<TransformConstraint, TransformConstraintData, TransformConstraintPose> {
@ -55,7 +54,7 @@ export class TransformConstraint extends Constraint<TransformConstraint, Transfo
this.bones = [] as BonePose[];
for (const boneData of data.bones)
this.bones.push(skeleton.bones[boneData.index].constrained);
this.bones.push(skeleton.bones[boneData.index].constrainedPose);
const source = skeleton.bones[data.source.index];
if (source == null) throw new Error("source cannot be null.");
@ -69,13 +68,13 @@ export class TransformConstraint extends Constraint<TransformConstraint, Transfo
}
update (skeleton: Skeleton, physics: Physics) {
const p = this.applied;
const p = this.appliedPose;
if (p.mixRotate === 0 && p.mixX === 0 && p.mixY === 0 && p.mixScaleX === 0 && p.mixScaleY === 0 && p.mixShearY === 0) return;
const data = this.data;
const localSource = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp;
const offsets = data.offsets;
const source = this.source.applied;
const source = this.source.appliedPose;
if (localSource) source.validateLocalTransform(skeleton);
const fromItems = data.properties;
const fn = data.properties.length, update = skeleton._update;

View File

@ -31,22 +31,22 @@ import type { Pose } from "./Pose"
/** Stores a pose for a transform constraint. */
export class TransformConstraintPose implements Pose<TransformConstraintPose> {
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
/** A percentage that controls the mix between the constrained and unconstrained rotation. */
mixRotate = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
/** A percentage that controls the mix between the constrained and unconstrained translation X. */
mixX = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
/** A percentage that controls the mix between the constrained and unconstrained translation Y. */
mixY = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
/** A percentage that controls the mix between the constrained and unconstrained scale X. */
mixScaleX = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
/** A percentage that controls the mix between the constrained and unconstrained scale X. */
mixScaleY = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
/** A percentage that controls the mix between the constrained and unconstrained shear Y. */
mixShearY = 0;
public set (pose: TransformConstraintPose) {

View File

@ -29,7 +29,6 @@
/** biome-ignore-all lint/complexity/noStaticOnlyClass: too much things to update */
import type { MixBlend } from "./Animation.js";
import type { Skeleton } from "./Skeleton.js";
export interface StringMap<T> {
@ -335,7 +334,7 @@ export class Utils {
}
// This function is used to fix WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
static webkit602BugfixHelper (alpha: number, blend: MixBlend) {
static webkit602BugfixHelper (alpha: number) {
}
static contains<T> (array: Array<T>, element: T, identity = true) {
@ -353,7 +352,7 @@ export class Utils {
export class DebugUtils {
static logBones (skeleton: Skeleton) {
for (let i = 0; i < skeleton.bones.length; i++) {
const bone = skeleton.bones[i].applied;
const bone = skeleton.bones[i].appliedPose;
console.log(`${bone.bone.data.name}, ${bone.a}, ${bone.b}, ${bone.c}, ${bone.d}, ${bone.worldX}, ${bone.worldY}`);
}
}

View File

@ -31,7 +31,7 @@ import type { Skeleton } from "src/Skeleton.js";
import type { Slot } from "../Slot.js";
import { type NumberArrayLike, Utils } from "../Utils.js";
/** The base class for all attachments. */
/** The base class for all attachments. Multiple {@link Skeleton} instances, slots, or skins can use the same attachments. */
export abstract class Attachment {
name: string;
@ -48,7 +48,7 @@ export abstract class Attachment {
abstract copy (): Attachment;
}
/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's
/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by
* {@link SlotPose.deform}. */
export abstract class VertexAttachment extends Attachment {
private static nextID = 0;
@ -56,13 +56,12 @@ export abstract class VertexAttachment extends Attachment {
/** The unique ID for this attachment. */
id = VertexAttachment.nextID++;
/** The bones which affect the {@link vertices}. The array entries are, for each vertex, the number of bones affecting
* the vertex followed by that many bone indices, which is the index of the bone in {@link Skeleton.bones}. Will be null
* if this attachment has no weights. */
/** The bones that affect the {@link #vertices}. The entries are, for each vertex, the number of bones affecting the vertex
* followed by that many bone indices, which is {@link Skeleton#getBones()} index. Null if this attachment has no weights. */
bones: Array<number> | null = null;
/** The vertex positions in the bone's coordinate system. For a non-weighted attachment, the values are `x,y`
* entries for each vertex. For a weighted attachment, the values are `x,y,weight` entries for each bone affecting
* entries for each vertex. For a weighted attachment, the values are `x,y,weight` triplets for each bone affecting
* each vertex. */
vertices: NumberArrayLike = [];
@ -74,27 +73,27 @@ export abstract class VertexAttachment extends Attachment {
super(name);
}
/** Transforms the attachment's local {@link #vertices} to world coordinates. If the slot's {@link SlotPose.deform} is
* not empty, it is used to deform the vertices.
*
* See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine
/** Transforms the attachment's local {@link #vertices} to world coordinates. If {@link SlotPose#getDeform()} is not empty, it
* is used to deform the vertices.
* <p>
* See <a href="https://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> in the Spine
* Runtimes Guide.
* @param start The index of the first {@link #vertices} value to transform. Each vertex has 2 values, x and y.
* @param count The number of world vertex values to output. Must be <= {@link #worldVerticesLength} - `start`.
* @param worldVertices The output world vertices. Must have a length >= `offset` + `count` *
* `stride` / 2.
* @param offset The `worldVertices` index to begin writing values.
* @param stride The number of `worldVertices` entries between the value pairs written. */
* @param count The number of world vertex values to output. Must be <= {@link #worldVerticesLength} - <code>start</code>.
* @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + <code>count</code> *
* <code>stride</code> / 2.
* @param offset The <code>worldVertices</code> index to begin writing values.
* @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number,
stride: number) {
count = offset + (count >> 1) * stride;
const deformArray = slot.applied.deform;
const deformArray = slot.appliedPose.deform;
let vertices = this.vertices;
const bones = this.bones;
if (!bones) {
if (deformArray.length > 0) vertices = deformArray;
const bone = slot.bone.applied;
const bone = slot.bone.appliedPose;
const x = bone.worldX;
const y = bone.worldY;
const a = bone.a, b = bone.b, c = bone.c, d = bone.d;
@ -118,7 +117,7 @@ export abstract class VertexAttachment extends Attachment {
let n = bones[v++];
n += v;
for (; v < n; v++, b += 3) {
const bone = skeletonBones[bones[v]].applied;
const bone = skeletonBones[bones[v]].appliedPose;
const vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight;
wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight;
@ -133,7 +132,7 @@ export abstract class VertexAttachment extends Attachment {
let n = bones[v++];
n += v;
for (; v < n; v++, b += 3, f += 2) {
const bone = skeletonBones[bones[v]].applied;
const bone = skeletonBones[bones[v]].appliedPose;
const vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight;
wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight;

View File

@ -33,7 +33,7 @@ import { type Attachment, VertexAttachment } from "./Attachment.js";
/** An attachment with vertices that make up a polygon used for clipping the rendering of other attachments. */
export class ClippingAttachment extends VertexAttachment {
/** Clipping is performed between the clipping polygon's slot and the end slot. Returns null if clipping is done until the end of
/** Clipping is performed between the clipping attachment's slot and the end slot. If null, clipping is done until the end of
* the skeleton's rendering. */
endSlot: SlotData | null = null;

View File

@ -34,13 +34,17 @@ export function isHasSequence (obj: unknown): obj is HasSequence {
return !!obj && typeof obj === "object" && "sequence" in obj && "updateSequence" in obj;
}
/** Interface for an attachment that gets 1 or more texture regions from a {@link Sequence}. */
export interface HasSequence {
/** The base path for the attachment's texture region. */
path?: string;
/** The color the attachment is tinted, to be combined with {@link SlotPose#getColor()}. */
color: Color;
/** Calls {@link Sequence#update(HasSequence)} on this attachment's sequence. */
updateSequence (): void;
/** The sequence that provides texture regions, UVs, and vertex offsets for rendering this attachment. */
sequence: Sequence;
}

View File

@ -35,7 +35,7 @@ import type { HasSequence } from "./HasSequence.js";
import type { Sequence } from "./Sequence.js";
/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
* supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
* supported. Each vertex has UVs (texture coordinates) and triangles that are used to map an image on to the mesh.
*
* See [Mesh attachments](http://esotericsoftware.com/spine-meshes) in the Spine User Guide. */
export class MeshAttachment extends VertexAttachment implements HasSequence {
@ -58,8 +58,8 @@ export class MeshAttachment extends VertexAttachment implements HasSequence {
private parentMesh: MeshAttachment | null = null;
/** Vertex index pairs describing edges for controlling triangulation, or be null if nonessential data was not exported. Mesh
* triangles never cross edges. Triangulation is not performed at runtime. */
/** Vertex index pairs describing edges for controlling triangulation, or null if nonessential data was not exported. Mesh
* triangles do not never cross edges. Triangulation is not performed at runtime. */
edges: Array<number> = [];
/** The width of the mesh's image. Available only when nonessential data was exported. */
@ -106,7 +106,7 @@ export class MeshAttachment extends VertexAttachment implements HasSequence {
/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
* {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
* parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
* parent mesh, but may have a different {@link #name} or {@link #path}, and therefore a different texture region. */
getParentMesh () {
return this.parentMesh;
}

View File

@ -41,8 +41,8 @@ export class PathAttachment extends VertexAttachment {
/** If true, the start and end knots are connected. */
closed = false;
/** If true, additional calculations are performed to make calculating positions along the path more accurate. If false, fewer
* calculations are performed but calculating positions along the path is less accurate. */
/** If true, additional calculations are performed to make computing positions along the path more accurate so movement along
* the path has a constant speed. */
constantSpeed = false;
/** The color of the path as it was in Spine. Available only when nonessential data was exported. Paths are not usually

View File

@ -37,8 +37,14 @@ import { type Attachment, VertexAttachment } from "./Attachment.js";
*
* See [Point Attachments](https://esotericsoftware.com/spine-points) in the Spine User Guide. */
export class PointAttachment extends VertexAttachment {
/** The local x position. */
x: number = 0;
/** The local y position. */
y: number = 0;
/** The local rotation in degrees, counter clockwise. */
rotation: number = 0;
/** The color of the point attachment as it was in Spine. Available only when nonessential data was exported. Point attachments
@ -49,12 +55,14 @@ export class PointAttachment extends VertexAttachment {
super(name);
}
/** Computes the world position from the local position. */
computeWorldPosition (bone: BonePose, point: Vector2) {
point.x = this.x * bone.a + this.y * bone.b + bone.worldX;
point.y = this.x * bone.c + this.y * bone.d + bone.worldY;
return point;
}
/** Computes the world rotation from the local rotation. */
computeWorldRotation (bone: BonePose) {
const r = this.rotation * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r);
const x = cos * bone.a + sin * bone.b;

View File

@ -53,7 +53,7 @@ export class RegionAttachment extends Attachment implements HasSequence {
/** The local scaleY. */
scaleY = 1;
/** The local rotation. */
/** The local rotation in degrees, counter clockwise. */
rotation = 0;
/** The width of the region attachment in Spine. */
@ -98,7 +98,7 @@ export class RegionAttachment extends Attachment implements HasSequence {
* @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
computeWorldVertices (slot: Slot, vertexOffsets: NumberArrayLike, worldVertices: NumberArrayLike, offset: number, stride: number) {
const bone = slot.bone.applied;
const bone = slot.bone.appliedPose;
const x = bone.worldX, y = bone.worldY;
const a = bone.a, b = bone.b, c = bone.c, d = bone.d;

View File

@ -34,24 +34,35 @@ import type { HasSequence } from "./HasSequence.js";
import { MeshAttachment } from "./MeshAttachment.js";
import { RegionAttachment } from "./RegionAttachment.js";
/** Holds texture regions, UVs, and vertex offsets for rendering a region or mesh attachment. {@link #getRegions() Regions} must
* be populated and {@link #update(HasSequence)} called before use. */
/** Holds texture regions, UVs, and vertex offsets for rendering a region or mesh attachment. {@link #regions Regions} must be
* populated and {@link #update(HasSequence)} called before use. */
export class Sequence {
private static _nextID = 0;
id = Sequence.nextID();
/** The list of texture regions this sequence will display. */
regions: Array<TextureRegion | null>;
readonly pathSuffix: boolean;
uvs?: NumberArrayLike[];
/** Returns vertex offsets from the center of a {@link RegionAttachment}. Invalid to call for a {@link MeshAttachment}. */
offsets?: number[][];
/** The starting number for the numeric {@link #getPath(String, int) path} suffix. */
start = 0;
/** The minimum number of digits in the numeric {@link #getPath(String, int) path} suffix, for zero padding. 0 for no zero
* padding. */
digits = 0;
/** The index of the region to show for the setup pose. */
setupIndex = 0;
/** @param count The number of texture regions this sequence will display.
* @param pathSuffix If true, the {@link #getPath(String, int) path} has a numeric suffix. If false, all regions will use the
* same path, so <code>count</code> should be 1. */
constructor (count: number, pathSuffix: boolean) {
this.regions = new Array<TextureRegion>(count);
this.pathSuffix = pathSuffix;
@ -108,6 +119,7 @@ export class Sequence {
}
}
/** Returns the {@link #regions} index for the {@link SlotPose#getSequenceIndex()}. */
resolveIndex (pose: SlotPose): number {
let index = pose.sequenceIndex;
if (index === -1) index = this.setupIndex;
@ -115,15 +127,19 @@ export class Sequence {
return index;
}
/** Returns the UVs for the specified index. {@link #regions Regions} must be populated and {@link #update(HasSequence)} called
* before calling this method. */
getUVs (index: number): Float32Array {
// biome-ignore lint/style/noNonNullAssertion: uvs are always defined after updateSequence
return this.uvs![index] as Float32Array;
}
public hasPathSuffix (): boolean {
/** Returns true if the {@link #getPath(String, int) path} has a numeric suffix. */
hasPathSuffix (): boolean {
return this.pathSuffix;
}
/** Returns the specified base path with an optional numeric suffix for the specified index. */
getPath (basePath: string, index: number): string {
if (!this.pathSuffix) return basePath;
let result = basePath;
@ -139,6 +155,7 @@ export class Sequence {
}
}
/** Controls how {@link Sequence#regions} are displayed over time. */
export enum SequenceMode {
hold = 0,
once = 1,

View File

@ -15,9 +15,9 @@ export * from './attachments/RegionAttachment.js';
export * from './attachments/Sequence.js';
export * from './Bone.js';
export * from './BoneData.js';
export * from './BoneLocal.js';
export * from './BonePose.js';
export * from './Constraint.js';
export * from './DrawOrder.js';
export * from './ConstraintData.js';
export * from './Event.js';
export * from './EventData.js';

View File

@ -294,9 +294,9 @@ export class SpineGameObject extends DepthMixin(
phaserWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
this.phaserWorldCoordinatesToSkeleton(point);
if (bone.parent) {
bone.parent.applied.worldToLocal(point as Vector2);
bone.parent.appliedPose.worldToLocal(point as Vector2);
} else {
bone.applied.worldToLocal(point as Vector2);
bone.appliedPose.worldToLocal(point as Vector2);
}
}
@ -416,7 +416,7 @@ export class SpineGameObject extends DepthMixin(
skeleton.scaleX = transform.scaleX;
skeleton.scaleY = transform.scaleY;
const root = skeleton.getRootBone() as Bone;
root.applied.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
root.appliedPose.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
this.skeleton.updateWorldTransform(Physics.update);
context.save();

View File

@ -294,9 +294,9 @@ export class SpineGameObject extends DepthMixin(
phaserWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
this.phaserWorldCoordinatesToSkeleton(point);
if (bone.parent) {
bone.parent.applied.worldToLocal(point as Vector2);
bone.parent.appliedPose.worldToLocal(point as Vector2);
} else {
bone.applied.worldToLocal(point as Vector2);
bone.appliedPose.worldToLocal(point as Vector2);
}
}
@ -439,7 +439,7 @@ export class SpineGameObject extends DepthMixin(
skeleton.scaleX = transform.scaleX;
skeleton.scaleY = transform.scaleY;
const root = skeleton.getRootBone() as Bone;
root.applied.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
root.appliedPose.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized;
this.skeleton.updateWorldTransform(Physics.update);
context.save();

View File

@ -373,7 +373,7 @@ export class Spine extends Container {
// dark tint can be enabled by options, otherwise is enable if at least one slot has tint black
this.darkTint = darkTint === undefined
? this.skeleton.slots.some(slot => !!slot.data.setup.darkColor)
? this.skeleton.slots.some(slot => !!slot.data.setupPose.darkColor)
: darkTint;
if (this.darkTint) this.slotMeshFactory = () => new DarkSlotMesh();
}
@ -563,12 +563,13 @@ export class Spine extends Container {
private updateSlotObject (element: { container: Container, followAttachmentTimeline: boolean }, slot: Slot, zIndex: number) {
const { container: slotObject, followAttachmentTimeline } = element
const pose = slot.applied;
const pose = slot.appliedPose;
const followAttachmentValue = followAttachmentTimeline ? Boolean(pose.attachment) : true;
slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue;
const drawOrder = this.skeleton.drawOrder.appliedPose;
slotObject.visible = drawOrder.includes(slot) && followAttachmentValue;
if (slotObject.visible) {
const applied = slot.bone.applied;
const applied = slot.bone.appliedPose;
const matrix = slotObject.localTransform;
matrix.a = applied.a;
@ -594,7 +595,7 @@ export class Spine extends Container {
}
if (!pixiMaskSource.computed) {
pixiMaskSource.computed = true;
const clippingAttachment = pixiMaskSource.slot.applied.attachment as ClippingAttachment;
const clippingAttachment = pixiMaskSource.slot.appliedPose.attachment as ClippingAttachment;
const worldVerticesLength = clippingAttachment.worldVerticesLength;
if (this.clippingVertAux.length < worldVerticesLength) this.clippingVertAux = new Float32Array(worldVerticesLength);
clippingAttachment.computeWorldVertices(this.skeleton, pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2);
@ -628,10 +629,11 @@ export class Spine extends Container {
let triangles: Array<number> | null = null;
let uvs: NumberArrayLike | null = null;
let pixiMaskSource: PixiMaskSource | null = null;
const drawOrder = this.skeleton.drawOrder;
const drawOrder = this.skeleton.drawOrder.appliedPose;
const slots = drawOrder;
for (let i = 0, n = drawOrder.length, slotObjectsCounter = 0; i < n; i++) {
const slot = drawOrder[i];
const slot = slots[i];
// render pixi object on the current slot on top of the slot attachment
const pixiObject = this.slotsObject.get(slot);
@ -642,7 +644,7 @@ export class Spine extends Container {
this.updateAndSetPixiMask(pixiMaskSource, pixiObject.container);
}
const pose = slot.applied;
const pose = slot.appliedPose;
const useDarkColor = !!pose.darkColor;
const vertexSize = useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE;
if (!slot.bone.active) {
@ -826,9 +828,9 @@ export class Spine extends Container {
if (!actualBone) throw Error(`Cannot set bone position, bone ${String(bone)} not found`);
Spine.vectorAux.set(position.x, position.y);
const applied = actualBone.applied;
const applied = actualBone.appliedPose;
if (actualBone.parent) {
const aux = actualBone.parent.applied.worldToLocal(Spine.vectorAux);
const aux = actualBone.parent.appliedPose.worldToLocal(Spine.vectorAux);
applied.x = aux.x;
applied.y = aux.y;
} else {
@ -855,8 +857,8 @@ export class Spine extends Container {
outPos = { x: 0, y: 0 };
}
outPos.x = actualBone.applied.worldX;
outPos.y = actualBone.applied.worldY;
outPos.x = actualBone.appliedPose.worldX;
outPos.y = actualBone.appliedPose.worldY;
return outPos;
}
@ -874,9 +876,9 @@ export class Spine extends Container {
pixiWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
this.pixiWorldCoordinatesToSkeleton(point);
if (bone.parent) {
bone.parent.applied.worldToLocal(point as Vector2);
bone.parent.appliedPose.worldToLocal(point as Vector2);
} else {
bone.applied.worldToLocal(point as Vector2);
bone.appliedPose.worldToLocal(point as Vector2);
}
}

View File

@ -244,7 +244,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
for (let i = 0, len = bones.length; i < len; i++) {
const bone = bones[i];
const boneLen = bone.data.length;
const applied = bone.applied;
const applied = bone.appliedPose;
const starX = skeletonX + applied.worldX;
const starY = skeletonY + applied.worldY;
const endX = skeletonX + boneLen * applied.a + applied.worldX;
@ -339,7 +339,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
for (let i = 0, len = slots.length; i < len; i++) {
const slot = slots[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment == null || !(attachment instanceof RegionAttachment)) {
continue;
@ -347,7 +347,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
const vertices = new Float32Array(8);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), vertices, 0, 2);
debugDisplayObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8)));
}
}
@ -365,7 +365,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
if (!slot.bone.active) {
continue;
}
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment == null || !(attachment instanceof MeshAttachment)) {
continue;
@ -421,7 +421,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
if (!slot.bone.active) {
continue;
}
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment == null || !(attachment instanceof ClippingAttachment)) {
continue;
@ -496,7 +496,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
if (!slot.bone.active) {
continue;
}
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment == null || !(attachment instanceof PathAttachment)) {
continue;

View File

@ -430,7 +430,7 @@ export class Spine extends ViewContainer {
// dark tint can be enabled by options, otherwise is enable if at least one slot has tint black
this.darkTint = darkTint === undefined
? this.skeleton.slots.some(slot => !!slot.data.setup.darkColor)
? this.skeleton.slots.some(slot => !!slot.data.setupPose.darkColor)
: darkTint;
const slots = this.skeleton.slots;
@ -471,9 +471,9 @@ export class Spine extends ViewContainer {
if (!bone) throw Error(`Cant set bone position, bone ${String(boneAux)} not found`);
vectorAux.set(position.x, position.y);
const applied = bone.applied;
const applied = bone.appliedPose;
if (bone.parent) {
const aux = bone.parent.applied.worldToLocal(vectorAux);
const aux = bone.parent.appliedPose.worldToLocal(vectorAux);
applied.x = aux.x;
applied.y = -aux.y;
@ -507,8 +507,8 @@ export class Spine extends ViewContainer {
outPos = { x: 0, y: 0 };
}
outPos.x = bone.applied.worldX;
outPos.y = bone.applied.worldY;
outPos.x = bone.appliedPose.worldX;
outPos.y = bone.appliedPose.worldY;
return outPos;
}
@ -556,7 +556,7 @@ export class Spine extends ViewContainer {
private validateAttachments () {
const currentDrawOrder = this.skeleton.drawOrder;
const currentDrawOrder = this.skeleton.drawOrder.appliedPose;
const lastAttachments = this._lastAttachments;
@ -566,7 +566,7 @@ export class Spine extends ViewContainer {
for (let i = 0; i < currentDrawOrder.length; i++) {
const slot = currentDrawOrder[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment) {
if (attachment !== lastAttachments[index]) {
@ -589,7 +589,7 @@ export class Spine extends ViewContainer {
private currentClippingSlot: SlotsToClipping | undefined;
private updateAndSetPixiMask (slot: Slot, last: boolean) {
// assign/create the currentClippingSlot
const pose = slot.applied;
const pose = slot.appliedPose;
const attachment = pose.attachment;
if (attachment && attachment instanceof ClippingAttachment) {
const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: [] as number[] });
@ -628,7 +628,7 @@ export class Spine extends ViewContainer {
}
// if current slot is the ending one of the currentClippingSlot mask, set currentClippingSlot to undefined
if (currentClippingSlot && (currentClippingSlot.slot.applied.attachment as ClippingAttachment).endSlot === slot.data) {
if (currentClippingSlot && (currentClippingSlot.slot.appliedPose.attachment as ClippingAttachment).endSlot === slot.data) {
this.currentClippingSlot = undefined;
}
@ -636,7 +636,7 @@ export class Spine extends ViewContainer {
if (last) {
for (const key in this.clippingSlotToPixiMasks) {
const clippingSlotToPixiMask = this.clippingSlotToPixiMasks[key];
if ((!(clippingSlotToPixiMask.slot.applied.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) {
if ((!(clippingSlotToPixiMask.slot.appliedPose.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) {
this.removeChild(clippingSlotToPixiMask.mask);
maskPool.free(clippingSlotToPixiMask.mask);
clippingSlotToPixiMask.mask = undefined;
@ -647,7 +647,7 @@ export class Spine extends ViewContainer {
}
private transformAttachments () {
const currentDrawOrder = this.skeleton.drawOrder;
const currentDrawOrder = this.skeleton.drawOrder.appliedPose;
const skeleton = this.skeleton;
for (let i = 0; i < currentDrawOrder.length; i++) {
@ -655,7 +655,7 @@ export class Spine extends ViewContainer {
this.updateAndSetPixiMask(slot, i === currentDrawOrder.length - 1);
const pose = slot.applied;
const pose = slot.appliedPose;
const attachment = pose.attachment;
if (attachment) {
@ -800,15 +800,15 @@ export class Spine extends ViewContainer {
private updateSlotObject (slotAttachment: { slot: Slot, container: Container, followAttachmentTimeline: boolean }) {
const { slot, container } = slotAttachment;
const pose = slot.applied;
const pose = slot.appliedPose;
const followAttachmentValue = slotAttachment.followAttachmentTimeline ? Boolean(pose.attachment) : true;
const slotAlpha = this.skeleton.color.a * pose.color.a;
container.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue
container.visible = this.skeleton.drawOrder.appliedPose.includes(slot) && followAttachmentValue
&& this.alpha > 0 && slotAlpha > 0;
if (container.visible) {
const applied = slot.bone.applied;
const applied = slot.bone.appliedPose;
const matrix = container.localTransform;
matrix.a = applied.a;
@ -1006,7 +1006,7 @@ export class Spine extends ViewContainer {
}
this._validateAndTransformAttachments();
const drawOrder = this.skeleton.drawOrder;
const drawOrder = this.skeleton.drawOrder.appliedPose;
const bounds = this._bounds;
bounds.clear();
@ -1014,7 +1014,7 @@ export class Spine extends ViewContainer {
for (let i = 0; i < drawOrder.length; i++) {
const slot = drawOrder[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) {
const cacheData = this._getCachedData(slot, attachment);
@ -1071,10 +1071,10 @@ export class Spine extends ViewContainer {
public pixiWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) {
this.pixiWorldCoordinatesToSkeleton(point);
if (bone.parent) {
bone.parent.applied.worldToLocal(point as Vector2);
bone.parent.appliedPose.worldToLocal(point as Vector2);
}
else {
bone.applied.worldToLocal(point as Vector2);
bone.appliedPose.worldToLocal(point as Vector2);
}
}

View File

@ -259,7 +259,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
for (let i = 0, len = bones.length; i < len; i++) {
const bone = bones[i];
const boneLen = bone.data.length;
const applied = bone.applied;
const applied = bone.appliedPose;
const starX = skeletonX + applied.worldX;
const starY = skeletonY + applied.worldY;
const endX = skeletonX + (boneLen * applied.a) + applied.worldX;
@ -360,7 +360,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
for (let i = 0, len = slots.length; i < len; i++) {
const slot = slots[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment === null || !(attachment instanceof RegionAttachment)) {
continue;
@ -368,7 +368,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
const vertices = new Float32Array(8);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), vertices, 0, 2);
debugDisplayObjects.regionAttachmentsShape.poly(Array.from(vertices.slice(0, 8)));
}
@ -388,7 +388,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
if (!slot.bone.active) {
continue;
}
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment === null || !(attachment instanceof MeshAttachment)) {
continue;
@ -448,7 +448,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
if (!slot.bone.active) {
continue;
}
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment === null || !(attachment instanceof ClippingAttachment)) {
continue;
@ -533,7 +533,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
if (!slot.bone.active) {
continue;
}
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment === null || !(attachment instanceof PathAttachment)) {
continue;

View File

@ -87,14 +87,14 @@ export class SpinePipe implements RenderPipe<Spine> {
// if the textures have changed, we need to rebuild the batch, but only if the texture is not already in the batch
else if (spine.spineTexturesDirty) {
// loop through and see if the textures have changed..
const drawOrder = spine.skeleton.drawOrder;
const drawOrder = spine.skeleton.drawOrder.appliedPose;
const gpuSpine = this.gpuSpineData[spine.uid];
if (!gpuSpine) return false;
for (let i = 0, n = drawOrder.length; i < n; i++) {
const slot = drawOrder[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) {
const cacheData = spine._getCachedData(slot, attachment);
@ -150,7 +150,7 @@ export class SpinePipe implements RenderPipe<Spine> {
const batcher = this.renderer.renderPipes.batch;
const drawOrder = spine.skeleton.drawOrder;
const drawOrder = spine.skeleton.drawOrder.appliedPose;
const roundPixels = (this.renderer._roundPixels | spine._roundPixels) as 0 | 1;
@ -161,7 +161,7 @@ export class SpinePipe implements RenderPipe<Spine> {
for (let i = 0, n = drawOrder.length; i < n; i++) {
const slot = drawOrder[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
const blendMode = spineBlendModeMap[slot.data.blendMode];
let skipRender = false;
@ -207,11 +207,11 @@ export class SpinePipe implements RenderPipe<Spine> {
spine.spineAttachmentsDirty = false;
spine.spineTexturesDirty = false;
const drawOrder = spine.skeleton.drawOrder;
const drawOrder = spine.skeleton.drawOrder.appliedPose;
for (let i = 0, n = drawOrder.length; i < n; i++) {
const slot = drawOrder[i];
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) {
const cacheData = spine._getCachedData(slot, attachment);

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { type Animation, AnimationState, AnimationStateData, AtlasAttachmentLoader, type Bone, Color, type Disposable, type Downloader, MathUtils, MixBlend, MixDirection, Physics, Skeleton, SkeletonBinary, type SkeletonData, SkeletonJson, type StringMap, type TextureAtlas, TextureFilter, TimeKeeper, type TrackEntry, Vector2 } from "@esotericsoftware/spine-core"
import { type Animation, AnimationState, AnimationStateData, AtlasAttachmentLoader, type Bone, Color, type Disposable, type Downloader, MathUtils, Physics, Skeleton, SkeletonBinary, type SkeletonData, SkeletonJson, type StringMap, type TextureAtlas, TextureFilter, TimeKeeper, type TrackEntry, Vector2 } from "@esotericsoftware/spine-core"
import { AssetManager, type GLTexture, Input, LoadingScreen, ManagedWebGLRenderingContext, ResizeMode, SceneRenderer, Vector3 } from "@esotericsoftware/spine-webgl"
export interface SpinePlayerConfig {
@ -602,7 +602,7 @@ export class SpinePlayer implements Disposable {
const bone = skeleton.findBone(controlBones[i]);
if (!bone) continue;
const distance = renderer.camera.worldToScreen(
coords.set(bone.applied.worldX, bone.applied.worldY, 0),
coords.set(bone.appliedPose.worldX, bone.appliedPose.worldY, 0),
canvas.clientWidth, canvas.clientHeight).distance(mouse);
if (distance < bestDistance) {
bestDistance = distance;
@ -632,9 +632,9 @@ export class SpinePlayer implements Disposable {
x = MathUtils.clamp(x + offset.x, 0, canvas.clientWidth)
y = MathUtils.clamp(y - offset.y, 0, canvas.clientHeight);
renderer.camera.screenToWorld(coords.set(x, y, 0), canvas.clientWidth, canvas.clientHeight);
const applied = target.applied;
const applied = target.appliedPose;
if (target.parent) {
target.parent.applied.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
target.parent.appliedPose.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y));
applied.x = position.x;
applied.y = position.y;
} else {
@ -801,7 +801,7 @@ export class SpinePlayer implements Disposable {
const tempArray = [0, 0];
for (let i = 0; i < steps; i++, time += stepTime) {
animation.apply(this.skeleton!, time, time, false, [], 1, MixBlend.setup, MixDirection.in, false);
animation.apply(this.skeleton!, time, time, false, [], 1, true, false, false, false);
this.skeleton!.updateWorldTransform(Physics.update);
this.skeleton!.getBounds(offset, size, tempArray, this.sceneRenderer!.skeletonRenderer.getSkeletonClipping());
@ -937,7 +937,7 @@ export class SpinePlayer implements Disposable {
if (!bone) continue;
const colorInner = selectedBones[i] ? BONE_INNER_OVER : BONE_INNER;
const colorOuter = selectedBones[i] ? BONE_OUTER_OVER : BONE_OUTER;
const applied = bone.applied;
const applied = bone.appliedPose;
renderer.circle(true, skeleton.x + applied.worldX, skeleton.y + applied.worldY, 20, colorInner);
renderer.circle(false, skeleton.x + applied.worldX, skeleton.y + applied.worldY, 20, colorOuter);
}

View File

@ -225,7 +225,7 @@ export class SkeletonMesh extends THREE.Object3D {
let triangles: Array<number> | null = null;
let uvs: NumberArrayLike | null = null;
const skeleton = this.skeleton;
const drawOrder = skeleton.drawOrder;
const drawOrder = skeleton.drawOrder.appliedPose;
let batch = this.nextBatch();
batch.begin();
let z = 0;
@ -237,7 +237,7 @@ export class SkeletonMesh extends THREE.Object3D {
clipper.clipEnd(slot);
continue;
}
const pose = slot.applied;
const pose = slot.appliedPose;
const attachment = pose.attachment;
let attachmentColor: Color | null;
let texture: ThreeJsTexture | null;

View File

@ -648,7 +648,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
// show skeleton root
const root = skeleton.getRootBone() as Bone;
renderer.circle(true, root.applied.x + worldOffsetX, root.applied.y + worldOffsetY, 10, red);
renderer.circle(true, root.appliedPose.x + worldOffsetX, root.appliedPose.y + worldOffsetY, 10, red);
// show shifted origin
renderer.circle(true, divOriginX, divOriginY, 10, green);
@ -671,7 +671,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
for (const boneFollower of widget.boneFollowerList) {
const { slot, bone, element, followVisibility, followRotation, followOpacity, followScale } = boneFollower;
const { worldX, worldY } = widget;
const applied = bone.applied;
const applied = bone.appliedPose;
this.worldToScreen(this.tempFollowBoneVector, applied.worldX + worldX, applied.worldY + worldY);
if (Number.isNaN(this.tempFollowBoneVector.x)) continue;
@ -691,7 +691,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr
element.style.display = ""
const pose = slot.applied;
const pose = slot.appliedPose;
if (followVisibility && !pose.attachment) {
element.style.opacity = "0";
} else if (followOpacity) {

View File

@ -36,8 +36,6 @@ import {
type Disposable,
type LoadingScreen,
MeshAttachment,
MixBlend,
MixDirection,
type NumberArrayLike,
Physics,
RegionAttachment,
@ -1149,7 +1147,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
private checkSlotInteraction (type: PointerEventTypesInput, originalEvent?: UIEvent) {
for (const [slot, interactionState] of this.pointerSlotEventCallbacks) {
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (!(attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) continue;
@ -1160,7 +1158,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
// we could probably cache the vertices from rendering if interaction with this slot is enabled
if (attachment instanceof RegionAttachment) {
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), vertices, 0, 2);
} else if (attachment instanceof MeshAttachment) {
attachment.computeWorldVertices(this.skeleton as Skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
hullLength = attachment.hullLength;
@ -1236,7 +1234,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
if (!slot) return;
if (hideAttachment) {
slot.applied.setAttachment(null);
slot.appliedPose.setAttachment(null);
}
element.style.position = 'absolute';
@ -1280,7 +1278,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0;
let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000;
for (let i = 0; i < steps; i++, time += stepTime) {
animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.in, false);
animation.apply(skeleton, time, time, false, [], 1, true, false, false, false);
skeleton.updateWorldTransform(Physics.update);
skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping());

View File

@ -74,7 +74,7 @@ export class SkeletonDebugRenderer implements Disposable {
const bone = bones[i];
if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue;
if (!bone.parent) continue;
const boneApplied = bone.applied;
const boneApplied = bone.appliedPose;
const x = bone.data.length * boneApplied.a + boneApplied.worldX;
const y = bone.data.length * boneApplied.c + boneApplied.worldY;
shapes.rectLine(true, boneApplied.worldX, boneApplied.worldY, x, y, this.boneWidth * this.scale);
@ -88,11 +88,11 @@ export class SkeletonDebugRenderer implements Disposable {
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (attachment instanceof RegionAttachment) {
const vertices = this.vertices;
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.appliedPose), vertices, 0, 2);
shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);
@ -106,7 +106,7 @@ export class SkeletonDebugRenderer implements Disposable {
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (!(attachment instanceof MeshAttachment)) continue;
const vertices = this.vertices;
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
@ -155,7 +155,7 @@ export class SkeletonDebugRenderer implements Disposable {
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (!(attachment instanceof PathAttachment)) continue;
let nn = attachment.worldVerticesLength;
const world = this.temp = Utils.setArraySize(this.temp, nn, 0);
@ -193,7 +193,7 @@ export class SkeletonDebugRenderer implements Disposable {
for (let i = 0, n = bones.length; i < n; i++) {
const bone = bones[i];
if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue;
const boneApplied = bone.applied;
const boneApplied = bone.appliedPose;
shapes.circle(true, boneApplied.worldX, boneApplied.worldY, 3 * this.scale, this.boneOriginColor, 8);
}
}
@ -204,7 +204,7 @@ export class SkeletonDebugRenderer implements Disposable {
for (let i = 0, n = slots.length; i < n; i++) {
const slot = slots[i];
if (!slot.bone.active) continue;
const attachment = slot.applied.attachment;
const attachment = slot.appliedPose.attachment;
if (!(attachment instanceof ClippingAttachment)) continue;
const nn = attachment.worldVerticesLength;
const world = this.temp = Utils.setArraySize(this.temp, nn, 0);

View File

@ -69,7 +69,7 @@ export class SkeletonRenderer {
const renderable: Renderable = this.renderable;
let uvs: NumberArrayLike;
let triangles: Array<number>;
const drawOrder = skeleton.drawOrder;
const drawOrder = skeleton.drawOrder.appliedPose;
let attachmentColor: Color;
const skeletonColor = skeleton.color;
const vertexSize = twoColorTint ? 12 : 8;
@ -95,7 +95,7 @@ export class SkeletonRenderer {
inRange = false;
}
const pose = slot.applied;
const pose = slot.appliedPose;
const attachment = pose.attachment;
let texture: GLTexture;
if (attachment instanceof RegionAttachment) {