mirror of
https://github.com/EsotericSoftware/spine-runtimes.git
synced 2026-03-26 22:49:01 +08:00
[ts] Ported latest changes from 29bf262d to b116c42f.
This commit is contained in:
parent
b116c42f8b
commit
9609fe6e5e
@ -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
@ -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;
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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.
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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.");
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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)) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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++)
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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. */
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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());
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user