diff --git a/spine-ts/spine-canvas/src/SkeletonRenderer.ts b/spine-ts/spine-canvas/src/SkeletonRenderer.ts index 837e16a8f..229f0e529 100644 --- a/spine-ts/spine-canvas/src/SkeletonRenderer.ts +++ b/spine-ts/spine-canvas/src/SkeletonRenderer.ts @@ -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 = 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; diff --git a/spine-ts/spine-core/src/Animation.ts b/spine-ts/spine-core/src/Animation.ts index 9fcdeaf4f..77e0392d2 100644 --- a/spine-ts/spine-core/src/Animation.ts +++ b/spine-ts/spine-core/src/Animation.ts @@ -31,26 +31,39 @@ import { type Attachment, VertexAttachment } from "./attachments/Attachment.js"; import { type HasSequence, isHasSequence } from "./attachments/HasSequence.js"; import { SequenceMode, SequenceModeValues } from "./attachments/Sequence.js"; import type { Inherit } from "./BoneData.js"; -import type { BoneLocal } from "./BoneLocal.js"; +import type { BonePose } from "./BonePose.js"; import type { Event } from "./Event.js"; -import { PathConstraint } from "./PathConstraint.js"; +import type { IkConstraint } from "./IkConstraint.js"; +import type { PathConstraint } from "./PathConstraint.js"; import type { PhysicsConstraint } from "./PhysicsConstraint.js"; import type { PhysicsConstraintData } from "./PhysicsConstraintData.js"; import type { PhysicsConstraintPose } from "./PhysicsConstraintPose.js"; import type { Skeleton } from "./Skeleton.js"; +import type { Slider } from "./Slider.js"; import type { Slot } from "./Slot.js"; import type { SlotPose } from "./SlotPose.js"; -import { MathUtils, type NumberArrayLike, StringSet, Utils } from "./Utils.js"; +import type { TransformConstraint } from "./TransformConstraint.js"; +import { type NumberArrayLike, StringSet, Utils } from "./Utils.js"; -/** A simple container for a list of timelines and a name. */ +/** Stores a list of timelines to animate a skeleton's pose over time. + * + * See Applying Animations in the Spine Runtimes + * Guide. */ export class Animation { - /** The animation's name, which is unique across all animations in the skeleton. */ + /** The animation's name, unique across all animations in the skeleton. + * + * See {@link SkeletonData#findAnimation(String)}. */ readonly name: string; - /** If the returned array or the timelines it contains are modified, {@link setTimelines()} must be called. */ + /** The duration of the animation in seconds, which is usually the highest time of all frames in the timelines. The duration is + * used to know when the animation has completed and, for animations that repeat, when it should loop back to the start. */ timelines: Array = []; readonly timelineIds: StringSet; + + /** {@link Skeleton#getBones()} indices that this animation's timelines modify. + * + * See {@link BoneTimeline#bones}. */ readonly bones: Array; /** The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is @@ -77,12 +90,15 @@ export class Animation { const items = timelines; for (let i = 0; i < n; i++) { const timeline = items[i]; - this.timelineIds.addAll(timeline.getPropertyIds()); + this.timelineIds.addAll(timeline.propertyIds); if (isBoneTimeline(timeline) && boneSet.add(timeline.boneIndex)) this.bones.push(timeline.boneIndex); } } + /** Returns true if this animation contains a timeline with any of the specified property IDs. + * + * See {@link Timeline#propertyIds()}. */ hasTimeline (ids: string[]): boolean { for (let i = 0; i < ids.length; i++) if (this.timelineIds.contains(ids[i])) return true; @@ -90,28 +106,32 @@ export class Animation { } /** Applies the animation's timelines to the specified skeleton. - * - * See Timeline {@link Timeline.apply}. - * @param skeleton The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton + *

+ * See {@link Timeline#apply(Skeleton, float, float, Array, float, boolean, boolean, boolean, boolean)} and + * Applying Animations in the Spine Runtimes + * Guide. + * @param skeleton The skeleton the animation is applied to. This provides access to the bones, slots, and other skeleton * components the timelines may change. - * @param lastTime The last time in seconds this animation was applied. Some timelines trigger only at specific times rather - * than every frame. Pass -1 the first time an animation is applied to ensure frame 0 is triggered. - * @param time The time in seconds the skeleton is being posed for. Most timelines find the frame before and the frame after - * this time and interpolate between the frame values. If beyond the {@link duration} and loop is - * true then the animation will repeat, else the last frame will be applied. - * @param loop If true, the animation repeats after the {@link duration}. - * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if no timelines - * fire events. - * @param alpha 0 applies the current or setup values (depending on blend). 1 applies the timeline values. Between - * 0 and 1 applies values between the current or setup values and the timeline values. By adjusting - * alpha over time, an animation can be mixed in or out. alpha can also be useful to apply - * animations on top of each other (layering). - * @param blend Controls how mixing is applied when alpha < 1. - * @param direction Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions, - * such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}. - * @param appliedPose True to to modify the applied pose. */ + * @param lastTime The last time in seconds this animation was applied. Some timelines trigger only at discrete times, in which + * case all keys are triggered between lastTime (exclusive) and time (inclusive). Pass -1 + * the first time an animation is applied to ensure frame 0 is triggered. + * @param time The time in seconds the skeleton is being posed for. Timelines find the frame before and after this time and + * interpolate between the frame values. + * @param loop True if time beyond the {@link #duration} repeats the animation, else the last frame is used. + * @param events If any events are fired, they are added to this list. Pass null to ignore fired events or if no timelines fire + * events. + * @param alpha 0 applies setup or current values (depending on fromSetup), 1 uses timeline values, and + * intermediate values interpolate between them. Adjusting alpha over time can mix an animation in or + * out. + * @param fromSetup If true, alpha transitions between setup and timeline values, setup values are used before the + * first frame (current values are not used). If false, alpha transitions between current and timeline + * values, no change is made before the first frame. + * @param add If true, for timelines that support it, their values are added to the setup or current values (depending on + * fromSetup). + * @param out True when the animation is mixing out, else it is mixing in. Used by timelines that perform instant transitions. + * @param appliedPose True to modify {@link Posed#appliedPose}, else {@link Posed#pose} is modified. */ apply (skeleton: Skeleton, lastTime: number, time: number, loop: boolean, events: Array | null, alpha: number, - blend: MixBlend, direction: MixDirection, appliedPose: boolean) { + fromSetup: boolean, add: boolean, out: boolean, appliedPose: boolean) { if (!skeleton) throw new Error("skeleton cannot be null."); if (loop && this.duration !== 0) { @@ -121,46 +141,10 @@ export class Animation { const timelines = this.timelines; for (let i = 0, n = timelines.length; i < n; i++) - timelines[i].apply(skeleton, lastTime, time, events, alpha, blend, direction, appliedPose); + timelines[i].apply(skeleton, lastTime, time, events, alpha, fromSetup, add, out, appliedPose); } } -/** Controls how a timeline value is mixed with the setup pose value or current pose value when a timeline's `alpha` - * < 1. - * - * See Timeline {@link Timeline.apply}. */ -export enum MixBlend { - /** Transitions between the setup and timeline values (the current value is not used). Before the first frame, the setup - * value is used. - *

- * setup is intended to transition to or from the setup pose, not for animations layered on top of others. */ - setup, - /** Transitions between the current and timeline values. Before the first frame, transitions between the current and setup - * values. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, use - * the setup value before the first frame. - *

- * first is intended for the first animations applied, not for animations layered on top of others. */ - first, - /** Transitions between the current and timeline values. No change is made before the first frame. - *

- * replace is intended for animations layered on top of others, not for the first animations applied. */ - replace, - /** Transitions between the current value and the current plus timeline values. No change is made before the first frame. - *

- * add is intended for animations layered on top of others, not for the first animations applied. - *

- * Properties set by additive animations must be set manually or by another animation before applying the additive - * animations, else the property values will increase each time the additive animations are applied. */ - add -} - -/** Indicates whether a timeline's `alpha` is mixing out over time toward 0 (the setup or current pose value) or - * mixing in toward 1 (the timeline's value). - * - * See Timeline {@link Timeline#apply}. */ -export enum MixDirection { - in, out -} export enum Property { rotate, @@ -196,10 +180,19 @@ export enum Property { sliderMix, } -/** The interface for all timelines. */ +/** The base class for all timelines. + *

+ * See Applying Animations in the Spine + * Runtimes Guide. */ export abstract class Timeline { - propertyIds: string[]; - frames: NumberArrayLike; + readonly propertyIds: string[]; + readonly frames: NumberArrayLike; + + /** True if this timeline supports additive blending. */ + additive = false; + + /** True if this timeline sets values instantaneously and does not support interpolation between frames. */ + instant = false; constructor (frameCount: number, ...propertyIds: string[]) { this.propertyIds = propertyIds; @@ -210,40 +203,47 @@ export abstract class Timeline { return this.propertyIds; } + /** The number of values stored per frame. */ getFrameEntries (): number { return 1; } + /** The number of frames in this timeline. */ getFrameCount () { return this.frames.length / this.getFrameEntries(); } + /** The duration of the timeline in seconds, which is usually the highest time of all frames in the timeline. */ getDuration (): number { return this.frames[this.frames.length - this.getFrameEntries()]; } /** Applies this timeline to the skeleton. - * @param skeleton The skeleton to which the timeline is being applied. This provides access to the bones, slots, and other - * skeleton components that the timeline may change. - * @param lastTime The last time in seconds this timeline was applied. Timelines such as {@link EventTimeline} trigger only - * at specific times rather than every frame. In that case, the timeline triggers everything between - * lastTime (exclusive) and time (inclusive). Pass -1 the first time an animation is - * applied to ensure frame 0 is triggered. - * @param time The time in seconds that the skeleton is being posed for. Most timelines find the frame before and the frame - * after this time and interpolate between the frame values. If beyond the last frame, the last frame will be - * applied. - * @param events If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline - * does not fire events. - * @param alpha 0 applies the current or setup value (depending on blend). 1 applies the timeline value. - * Between 0 and 1 applies a value between the current or setup value and the timeline value. By adjusting - * alpha over time, an animation can be mixed in or out. alpha can also be useful to - * apply animations on top of each other (layering). - * @param blend Controls how mixing is applied when alpha < 1. - * @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, - * such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, and others such as {@link ScaleTimeline}. - * @param appliedPose True to to modify the applied pose. */ + *

+ * See Applying Animations in the Spine + * Runtimes Guide. + * @param skeleton The skeleton the timeline is applied to. This provides access to the bones, slots, and other skeleton + * components the timelines may change. + * @param lastTime The last time in seconds this timeline was applied. Some timelines trigger only at discrete times, in + * which case all keys are triggered between lastTime (exclusive) and time (inclusive). + * Pass -1 the first time a timeline is applied to ensure frame 0 is triggered. + * @param time The time in seconds the skeleton is being posed for. Timelines find the frame before and after this time and + * interpolate between the frame values. + * @param events If any events are fired, they are added to this list. Pass null to ignore fired events or if no timelines + * fire events. + * @param alpha 0 applies setup or current values (depending on fromSetup), 1 uses timeline values, and + * intermediate values interpolate between them. Adjusting alpha over time can mix a timeline in or + * out. + * @param fromSetup If true, alpha transitions between setup and timeline values, setup values are used before + * the first frame (current values are not used). If false, alpha transitions between current and + * timeline values, no change is made before the first frame. + * @param add If true, for timelines that support it, their values are added to the setup or current values (depending on + * fromSetup). + * @param out True when the animation is mixing out, else it is mixing in. Used by timelines that perform instant + * transitions. + * @param appliedPose True to modify {@link Posed#appliedPose}, else {@link Posed#pose} is modified. */ abstract apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, - blend: MixBlend, direction: MixDirection, appliedPose: boolean): void; + fromSetup: boolean, add: boolean, out: boolean, appliedPose: boolean): void; /** Linear search using the specified stride (default 1). * @param time Must be >= the first value in frames. @@ -256,7 +256,7 @@ export abstract class Timeline { } } -/** An interface for timelines which change the property of a slot. */ +/** An interface for timelines that change a slot's properties. */ export interface SlotTimeline { /** The index of the slot in {@link Skeleton.slots} that will be changed when this timeline is applied. */ slotIndex: number; @@ -334,7 +334,7 @@ export abstract class CurveTimeline extends Timeline { } /** Returns the Bezier interpolated value for the specified time. - * @param frameIndex The index into {@link #getFrames()} for the values of the frame before time. + * @param frameIndex The index into {@link #frames} for the values of the frame before time. * @param valueOffset The offset from frameIndex to the value this curve is used for. * @param i The index of the Bezier segments. See {@link #getCurveType(int)}. */ getBezierValue (time: number, frameIndex: number, valueOffset: number, i: number) { @@ -356,6 +356,7 @@ export abstract class CurveTimeline extends Timeline { } } +/** The base class for a {@link CurveTimeline} that sets one property with a curve. */ export abstract class CurveTimeline1 extends CurveTimeline { constructor (frameCount: number, bezierCount: number, propertyId: string) { super(frameCount, bezierCount, propertyId); @@ -397,102 +398,102 @@ export abstract class CurveTimeline1 extends CurveTimeline { return this.getBezierValue(time, i, 1/*VALUE*/, curveType - 2/*BEZIER*/); } - getRelativeValue (time: number, alpha: number, blend: MixBlend, current: number, setup: number) { - if (time < this.frames[0]) { - switch (blend) { - case MixBlend.setup: - return setup; - case MixBlend.first: - return current + (setup - current) * alpha; - } - return current; - } + /** Returns the interpolated value for properties relative to the setup value. The timeline value is added to the setup + * value, rather than replacing it. + *

+ * See {@link Timeline#apply(Skeleton, float, float, Array, float, boolean, boolean, boolean, boolean)}. + * @param current The current value for the property. + * @param setup The setup value for the property. */ + getRelativeValue (time: number, alpha: number, fromSetup: boolean, add: boolean, current: number, setup: number) { + if (time < this.frames[0]) return fromSetup ? setup : current; const value = this.getCurveValue(time); - switch (blend) { - case MixBlend.setup: return setup + value * alpha; - case MixBlend.first: - case MixBlend.replace: return current + (value + setup - current) * alpha; - case MixBlend.add: return current + value * alpha; - } + return fromSetup ? setup + value * alpha : current + (add ? value : value + setup - current) * alpha; } - getAbsoluteValue (time: number, alpha: number, blend: MixBlend, current: number, setup: number, value?: number) { + /** Returns the interpolated value for properties set as absolute values. The timeline value replaces the setup value, + * rather than being relative to it. + *

+ * See {@link Timeline#apply(Skeleton, float, float, Array, float, boolean, boolean, boolean, boolean)}. + * @param current The current value for the property. + * @param setup The setup value for the property. */ + getAbsoluteValue (time: number, alpha: number, fromSetup: boolean, add: boolean, current: number, setup: number): number; + + /** Returns the interpolated value for properties set as absolute values, using the specified timeline value rather than + * calling {@link #getCurveValue(float)}. + *

+ * See {@link Timeline#apply(Skeleton, float, float, Array, float, boolean, boolean, boolean, boolean)}. + * @param current The current value for the property. + * @param setup The setup value for the property. + * @param value The timeline value to apply. */ + getAbsoluteValue (time: number, alpha: number, fromSetup: boolean, add: boolean, current: number, setup: number, value: number): number; + + getAbsoluteValue (time: number, alpha: number, fromSetup: boolean, add: boolean, current: number, setup: number, value?: number) { if (value === undefined) - return this.getAbsoluteValue1(time, alpha, blend, current, setup); + return this.getAbsoluteValue1(time, alpha, fromSetup, add, current, setup); else - return this.getAbsoluteValue2(time, alpha, blend, current, setup, value); + return this.getAbsoluteValue2(time, alpha, fromSetup, add, current, setup, value); } - private getAbsoluteValue1 (time: number, alpha: number, blend: MixBlend, current: number, setup: number) { - if (time < this.frames[0]) { - switch (blend) { - case MixBlend.setup: return setup; - case MixBlend.first: return current + (setup - current) * alpha; - default: return current; - } - } + private getAbsoluteValue1 (time: number, alpha: number, fromSetup: boolean, add: boolean, current: number, setup: number) { + if (time < this.frames[0]) return fromSetup ? setup : current; const value = this.getCurveValue(time); - switch (blend) { - case MixBlend.setup: return setup + (value - setup) * alpha; - case MixBlend.first: - case MixBlend.replace: return current + (value - current) * alpha; - case MixBlend.add: return current + value * alpha; - } + return fromSetup ? setup + (value - setup) * alpha : current + (add ? value : value - current) * alpha; } - private getAbsoluteValue2 (time: number, alpha: number, blend: MixBlend, current: number, setup: number, value: number) { - if (time < this.frames[0]) { - switch (blend) { - case MixBlend.setup: return setup; - case MixBlend.first: return current + (setup - current) * alpha; - default: return current; - } - } - switch (blend) { - case MixBlend.setup: return setup + (value - setup) * alpha; - case MixBlend.first: - case MixBlend.replace: return current + (value - current) * alpha; - case MixBlend.add: return current + value * alpha; - } + private getAbsoluteValue2 (time: number, alpha: number, fromSetup: boolean, add: boolean, current: number, setup: number, value: number) { + if (time < this.frames[0]) return fromSetup ? setup : current; + return fromSetup ? setup + (value - setup) * alpha : current + (add ? value : value - current) * alpha; } - getScaleValue (time: number, alpha: number, blend: MixBlend, direction: MixDirection, current: number, setup: number) { - const frames = this.frames; - if (time < frames[0]) { - switch (blend) { - case MixBlend.setup: return setup; - case MixBlend.first: return current + (setup - current) * alpha; - default: return current; - } - } + /** Returns the interpolated value for scale properties. The timeline and setup values are multiplied and sign adjusted. + * + * See {@link Timeline#apply(Skeleton, float, float, Array, float, boolean, boolean, boolean, boolean)}. + * @param current The current value for the property. + * @param setup The setup value for the property. */ + getScaleValue (time: number, alpha: number, fromSetup: boolean, add: boolean, out: boolean, current: number, setup: number) { + if (time < this.frames[0]) return fromSetup ? setup : current; const value = this.getCurveValue(time) * setup; - if (alpha === 1) return blend === MixBlend.add ? current + value - setup : value; - // Mixing out uses sign of setup or current pose, else use sign of key. - if (direction === MixDirection.out) { - switch (blend) { - case MixBlend.setup: - return setup + (Math.abs(value) * MathUtils.signum(setup) - setup) * alpha; - case MixBlend.first: - case MixBlend.replace: - return current + (Math.abs(value) * MathUtils.signum(current) - current) * alpha; - } - } else { - let s = 0; - switch (blend) { - case MixBlend.setup: - s = Math.abs(setup) * MathUtils.signum(value); - return s + (value - s) * alpha; - case MixBlend.first: - case MixBlend.replace: - s = Math.abs(current) * MathUtils.signum(value); - return s + (value - s) * alpha; - } - } - return current + (value - setup) * alpha; + if (alpha === 1 && !add) return value; + let base = fromSetup ? setup : current; + if (add) return base + (value - setup) * alpha; + if (out) return base + (Math.abs(value) * Math.sign(base) - base) * alpha; + base = Math.abs(base) * Math.sign(value); + return base + (value - base) * alpha; } } -/** The base class for a {@link CurveTimeline} that is a {@link BoneTimeline} and sets two properties. */ +/** An interface for timelines that change a bone's properties. */ +export interface BoneTimeline { + /** The index of the bone in {@link Skeleton.bones} that is changed by this timeline. */ + boneIndex: number; +} + +export function isBoneTimeline (obj: Timeline & Partial): obj is Timeline & BoneTimeline { + return typeof obj === 'object' && obj !== null && typeof obj.boneIndex === 'number'; +} + +/** The base class for timelines that change 1 bone property with a curve. */ +export abstract class BoneTimeline1 extends CurveTimeline1 implements BoneTimeline { + readonly boneIndex: number; + + constructor (frameCount: number, bezierCount: number, boneIndex: number, property: Property) { + super(frameCount, bezierCount, `${property}|${boneIndex}`); + this.boneIndex = boneIndex; + this.additive = true; + } + + public apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, + fromSetup: boolean, add: boolean, out: boolean, appliedPose: boolean) { + const bone = skeleton.bones[this.boneIndex]; + if (bone.active) + this.apply1(appliedPose ? bone.appliedPose : bone.pose, bone.data.setupPose, time, alpha, fromSetup, add, out); + } + + protected abstract apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean): void; +} + +/** The base class for timelines that change two bone properties with a curve. */ export abstract class BoneTimeline2 extends CurveTimeline implements BoneTimeline { readonly boneIndex; @@ -501,6 +502,7 @@ export abstract class BoneTimeline2 extends CurveTimeline implements BoneTimelin constructor (frameCount: number, bezierCount: number, boneIndex: number, property1: Property, property2: Property) { super(frameCount, bezierCount, `${property1}|${boneIndex}`, `${property2}|${boneIndex}`); this.boneIndex = boneIndex; + this.additive = true; } getFrameEntries () { @@ -518,71 +520,41 @@ export abstract class BoneTimeline2 extends CurveTimeline implements BoneTimelin } apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, - blend: MixBlend, direction: MixDirection, appliedPose: boolean): void { + fromSetup: boolean, add: boolean, out: boolean, appliedPose: boolean): void { const bone = skeleton.bones[this.boneIndex]; - if (bone.active) this.apply1(appliedPose ? bone.applied : bone.pose, bone.data.setup, time, alpha, blend, direction); + if (bone.active) + this.apply1(appliedPose ? bone.appliedPose : bone.pose, bone.data.setupPose, time, alpha, fromSetup, add, out); } - protected abstract apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, - direction: MixDirection): void; + protected abstract apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean,): void; } -export interface BoneTimeline { - /** The index of the bone in {@link Skeleton.bones} that will be changed when this timeline is applied. */ - boneIndex: number; -} - -export function isBoneTimeline (obj: Timeline & Partial): obj is Timeline & BoneTimeline { - return typeof obj === 'object' && obj !== null && typeof obj.boneIndex === 'number'; -} - -export abstract class BoneTimeline1 extends CurveTimeline1 implements BoneTimeline { - readonly boneIndex: number; - - constructor (frameCount: number, bezierCount: number, boneIndex: number, property: Property) { - super(frameCount, bezierCount, `${property}|${boneIndex}`); - this.boneIndex = boneIndex; - } - - public apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, - blend: MixBlend, direction: MixDirection, appliedPose: boolean) { - - const bone = skeleton.bones[this.boneIndex]; - if (bone.active) this.apply1(appliedPose ? bone.applied : bone.pose, bone.data.setup, time, alpha, blend, direction); - } - - protected abstract apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, - direction: MixDirection): void; -} - -/** Changes a bone's local {@link Bone#rotation}. */ +/** Changes {@link BonePose#rotation}. */ export class RotateTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.rotate); } - apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.rotation = this.getRelativeValue(time, alpha, blend, pose.rotation, setup.rotation); + apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.rotation = this.getRelativeValue(time, alpha, fromSetup, add, pose.rotation, setup.rotation); } } -/** Changes a bone's local {@link BoneLocal.x} and {@link BoneLocal.y}. */ +/** Changes {@link BonePose.x} and {@link BonePose.y}. */ export class TranslateTimeline extends BoneTimeline2 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.x, Property.y); } - apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { + apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { const frames = this.frames; if (time < frames[0]) { - switch (blend) { - case MixBlend.setup: - pose.x = setup.x; - pose.y = setup.y; - return; - case MixBlend.first: - pose.x += (setup.x - pose.x) * alpha; - pose.y += (setup.y - pose.y) * alpha; + if (fromSetup) { + pose.x = setup.x; + pose.y = setup.y; } return; } @@ -609,62 +581,56 @@ export class TranslateTimeline extends BoneTimeline2 { y = this.getBezierValue(time, i, 2/*VALUE2*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); } - switch (blend) { - case MixBlend.setup: - pose.x = setup.x + x * alpha; - pose.y = setup.y + y * alpha; - break; - case MixBlend.first: - case MixBlend.replace: - pose.x += (setup.x + x - pose.x) * alpha; - pose.y += (setup.y + y - pose.y) * alpha; - break; - case MixBlend.add: - pose.x += x * alpha; - pose.y += y * alpha; + if (fromSetup) { + pose.x = setup.x + x * alpha; + pose.y = setup.y + y * alpha; + } else if (add) { + pose.x += x * alpha; + pose.y += y * alpha; + } else { + pose.x += (setup.x + x - pose.x) * alpha; + pose.y += (setup.y + y - pose.y) * alpha; } } } -/** Changes a bone's local {@link BoneLocal.x}. */ +/** Changes {@link BonePose.x}. */ export class TranslateXTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.x); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.x = this.getRelativeValue(time, alpha, blend, pose.x, setup.x); + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.x = this.getRelativeValue(time, alpha, fromSetup, add, pose.x, setup.x); } } -/** Changes a bone's local {@link BoneLocal.y}. */ +/** Changes {@link BonePose.y}. */ export class TranslateYTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.y); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.y = this.getRelativeValue(time, alpha, blend, pose.y, setup.y); + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.y = this.getRelativeValue(time, alpha, fromSetup, add, pose.y, setup.y); } } -/** Changes a bone's local {@link BoneLocal.scaleX} and {@link BoneLocal.scaleY}. */ +/** Changes {@link BonePose.scaleX} and {@link BonePose.scaleY}. */ export class ScaleTimeline extends BoneTimeline2 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.scaleX, Property.scaleY); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { const frames = this.frames; if (time < frames[0]) { - switch (blend) { - case MixBlend.setup: - pose.scaleX = setup.scaleX; - pose.scaleY = setup.scaleY; - return; - case MixBlend.first: - pose.scaleX += (setup.scaleX - pose.scaleX) * alpha; - pose.scaleY += (setup.scaleY - pose.scaleY) * alpha; + if (fromSetup) { + pose.scaleX = setup.scaleX; + pose.scaleY = setup.scaleY; } return; } @@ -693,98 +659,70 @@ export class ScaleTimeline extends BoneTimeline2 { x *= setup.scaleX; y *= setup.scaleY; - if (alpha === 1) { - if (blend === MixBlend.add) { - pose.scaleX += x - setup.scaleX; - pose.scaleY += y - setup.scaleY; - } else { - pose.scaleX = x; - pose.scaleY = y; - } + if (alpha === 1 && !add) { + pose.scaleX = x; + pose.scaleY = y; } else { let bx = 0, by = 0; - if (direction === MixDirection.out) { - switch (blend) { - case MixBlend.setup: - bx = setup.scaleX; - by = setup.scaleY; - pose.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; - pose.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; - break; - case MixBlend.first: - case MixBlend.replace: - bx = pose.scaleX; - by = pose.scaleY; - pose.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; - pose.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; - break; - case MixBlend.add: - pose.scaleX += (x - setup.scaleX) * alpha; - pose.scaleY += (y - setup.scaleY) * alpha; - } + if (fromSetup) { + bx = setup.scaleX; + by = setup.scaleY; } else { - switch (blend) { - case MixBlend.setup: - bx = Math.abs(setup.scaleX) * MathUtils.signum(x); - by = Math.abs(setup.scaleY) * MathUtils.signum(y); - pose.scaleX = bx + (x - bx) * alpha; - pose.scaleY = by + (y - by) * alpha; - break; - case MixBlend.first: - case MixBlend.replace: - bx = Math.abs(pose.scaleX) * MathUtils.signum(x); - by = Math.abs(pose.scaleY) * MathUtils.signum(y); - pose.scaleX = bx + (x - bx) * alpha; - pose.scaleY = by + (y - by) * alpha; - break; - case MixBlend.add: - pose.scaleX += (x - setup.scaleX) * alpha; - pose.scaleY += (y - setup.scaleY) * alpha; - } + bx = pose.scaleX; + by = pose.scaleY; + } + if (add) { + pose.scaleX = bx + (x - setup.scaleX) * alpha; + pose.scaleY = by + (y - setup.scaleY) * alpha; + } else if (out) { + pose.scaleX = bx + (Math.abs(x) * Math.sign(bx) - bx) * alpha; + pose.scaleY = by + (Math.abs(y) * Math.sign(by) - by) * alpha; + } else { + bx = Math.abs(bx) * Math.sign(x); + by = Math.abs(by) * Math.sign(y); + pose.scaleX = bx + (x - bx) * alpha; + pose.scaleY = by + (y - by) * alpha; } } } } -/** Changes a bone's local {@link BoneLocal.scaleX}. */ +/** Changes a {@link BonePose.scaleX}. */ export class ScaleXTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.scaleX); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.scaleX = this.getScaleValue(time, alpha, blend, direction, pose.scaleX, setup.scaleX); + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.scaleX = this.getScaleValue(time, alpha, fromSetup, add, out, pose.scaleX, setup.scaleX); } } -/** Changes a bone's local {@link BoneLocal.scaleY}. */ +/** Changes a {@link BonePose.scaleY}. */ export class ScaleYTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.scaleY); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.scaleY = this.getScaleValue(time, alpha, blend, direction, pose.scaleY, setup.scaleY); + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.scaleY = this.getScaleValue(time, alpha, fromSetup, add, out, pose.scaleY, setup.scaleY); } } -/** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ +/** Changes {@link Bone#shearX} and {@link Bone#shearY}. */ export class ShearTimeline extends BoneTimeline2 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.shearX, Property.shearY); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, out: boolean,) { const frames = this.frames; if (time < frames[0]) { - switch (blend) { - case MixBlend.setup: - pose.shearX = setup.shearX; - pose.shearY = setup.shearY; - return; - case MixBlend.first: - pose.shearX += (setup.shearX - pose.shearX) * alpha; - pose.shearY += (setup.shearY - pose.shearY) * alpha; + if (fromSetup) { + pose.shearX = setup.shearX; + pose.shearY = setup.shearY; } return; } @@ -811,52 +749,51 @@ export class ShearTimeline extends BoneTimeline2 { y = this.getBezierValue(time, i, 2/*VALUE2*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); } - switch (blend) { - case MixBlend.setup: - pose.shearX = setup.shearX + x * alpha; - pose.shearY = setup.shearY + y * alpha; - break; - case MixBlend.first: - case MixBlend.replace: - pose.shearX += (setup.shearX + x - pose.shearX) * alpha; - pose.shearY += (setup.shearY + y - pose.shearY) * alpha; - break; - case MixBlend.add: - pose.shearX += x * alpha; - pose.shearY += y * alpha; + if (fromSetup) { + pose.shearX = setup.shearX + x * alpha; + pose.shearY = setup.shearY + y * alpha; + } else if (add) { + pose.shearX += x * alpha; + pose.shearY += y * alpha; + } else { + pose.shearX += (setup.shearX + x - pose.shearX) * alpha; + pose.shearY += (setup.shearY + y - pose.shearY) * alpha; } } } -/** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ +/** Changes {@link Bone#shearX} and {@link Bone#shearY}. */ export class ShearXTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.shearX); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.shearX = this.getRelativeValue(time, alpha, blend, pose.shearX, setup.shearX); + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.shearX = this.getRelativeValue(time, alpha, fromSetup, add, pose.shearX, setup.shearX); } } -/** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ +/** Changes {@link Bone#shearX} and {@link Bone#shearY}. */ export class ShearYTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { super(frameCount, bezierCount, boneIndex, Property.shearY); } - protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha: number, blend: MixBlend, direction: MixDirection) { - pose.shearY = this.getRelativeValue(time, alpha, blend, pose.shearY, setup.shearY); + protected apply1 (pose: BonePose, setup: BonePose, time: number, alpha: number, fromSetup: boolean, add: boolean, + out: boolean) { + pose.shearY = this.getRelativeValue(time, alpha, fromSetup, add, pose.shearY, setup.shearY); } } -/** Changes a bone's {@link BoneLocal.inherit}. */ +/** Changes {@link BonePose.inherit}. */ export class InheritTimeline extends Timeline implements BoneTimeline { readonly boneIndex: number; constructor (frameCount: number, boneIndex: number) { super(frameCount, `${Property.inherit}|${boneIndex}`); this.boneIndex = boneIndex; + this.instant = true; } public getFrameEntries () { @@ -872,26 +809,25 @@ export class InheritTimeline extends Timeline implements BoneTimeline { this.frames[frame + 1/*INHERIT*/] = inherit; } - public apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + public apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { const bone = skeleton.bones[this.boneIndex]; if (!bone.active) return; - const pose = appliedPose ? bone.applied : bone.pose; + const pose = appliedPose ? bone.appliedPose : bone.pose; - if (direction === MixDirection.out) { - if (blend === MixBlend.setup) pose.inherit = bone.data.setup.inherit; - return; + if (out) { + if (fromSetup) pose.inherit = bone.data.setupPose.inherit; + } else { + const frames = this.frames; + if (time < frames[0]) { + if (fromSetup) pose.inherit = bone.data.setupPose.inherit; + } else + pose.inherit = this.frames[Timeline.search(frames, time, 2/*ENTRIES*/) + 1/*INHERIT*/]; } - - const frames = this.frames; - if (time < frames[0]) { - if (blend === MixBlend.setup || blend === MixBlend.first) pose.inherit = bone.data.setup.inherit; - } else - pose.inherit = this.frames[Timeline.search(frames, time, 2/*ENTRIES*/) + 1/*INHERIT*/]; } } - +/** The base class for timelines that change any number of slot properties with a curve. */ export abstract class SlotCurveTimeline extends CurveTimeline implements SlotTimeline { readonly slotIndex: number; @@ -900,17 +836,17 @@ export abstract class SlotCurveTimeline extends CurveTimeline implements SlotTim this.slotIndex = slotIndex; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { const slot = skeleton.slots[this.slotIndex]; - if (slot.bone.active) this.apply1(slot, appliedPose ? slot.applied : slot.pose, time, alpha, blend); + if (slot.bone.active) this.apply1(slot, appliedPose ? slot.appliedPose : slot.pose, time, alpha, fromSetup, add); } - protected abstract apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend): void; + protected abstract apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, fromSetup: boolean, add: boolean): void; } -/** Changes a slot's {@link SlotPose.color}. */ +/** Changes {@link SlotPose.color}. */ export class RGBATimeline extends SlotCurveTimeline { constructor (frameCount: number, bezierCount: number, slotIndex: number) { super(frameCount, bezierCount, slotIndex, // @@ -932,16 +868,11 @@ export class RGBATimeline extends SlotCurveTimeline { this.frames[frame + 4/*A*/] = a; } - protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, fromSetup: boolean, add: boolean) { const color = pose.color; const frames = this.frames; if (time < frames[0]) { - const setup = slot.data.setup.color; - switch (blend) { - case MixBlend.setup: color.setFromColor(setup); break; - case MixBlend.first: color.add((setup.r - color.r) * alpha, (setup.g - color.g) * alpha, (setup.b - color.b) * alpha, - (setup.a - color.a) * alpha); break; - } + if (fromSetup) color.setFromColor(slot.data.setupPose.color); return; } @@ -977,8 +908,8 @@ export class RGBATimeline extends SlotCurveTimeline { if (alpha === 1) color.set(r, g, b, a); else { - if (blend === MixBlend.setup) { - const setup = slot.data.setup.color; + if (fromSetup) { + const setup = slot.data.setupPose.color; color.set(setup.r + (r - setup.r) * alpha, setup.g + (g - setup.g) * alpha, setup.b + (b - setup.b) * alpha, setup.a + (a - setup.a) * alpha); } else @@ -987,7 +918,7 @@ export class RGBATimeline extends SlotCurveTimeline { } } -/** Changes the RGB for a slot's {@link SlotPose.color}. */ +/** Changes RGB for a slot's {@link SlotPose.color}. */ export class RGBTimeline extends SlotCurveTimeline { constructor (frameCount: number, bezierCount: number, slotIndex: number) { super(frameCount, bezierCount, slotIndex, `${Property.rgb}|${slotIndex}`); @@ -1006,63 +937,54 @@ export class RGBTimeline extends SlotCurveTimeline { this.frames[frame + 3/*B*/] = b; } - protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, fromSetup: boolean, add: boolean) { const color = pose.color; let r = 0, g = 0, b = 0; const frames = this.frames; if (time < frames[0]) { - const setup = slot.data.setup.color; - switch (blend) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime - case MixBlend.setup: - color.r = setup.r; - color.g = setup.g; - color.b = setup.b; - // Fall through. - // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime - default: - return; - case MixBlend.first: - r = color.r + (setup.r - color.r) * alpha; - g = color.g + (setup.g - color.g) * alpha; - b = color.b + (setup.b - color.b) * alpha; + if (fromSetup) { + const setup = slot.data.setupPose.color; + color.r = setup.r; + color.g = setup.g; + color.b = setup.b; } - } else { - const i = Timeline.search(frames, time, 4/*ENTRIES*/); - const curveType = this.curves[i >> 2]; - switch (curveType) { - case 0/*LINEAR*/: { - const before = frames[i]; - r = frames[i + 1/*R*/]; - g = frames[i + 2/*G*/]; - b = frames[i + 3/*B*/]; - const t = (time - before) / (frames[i + 4/*ENTRIES*/] - before); - r += (frames[i + 4/*ENTRIES*/ + 1/*R*/] - r) * t; - g += (frames[i + 4/*ENTRIES*/ + 2/*G*/] - g) * t; - b += (frames[i + 4/*ENTRIES*/ + 3/*B*/] - b) * t; - break; - } - case 1/*STEPPED*/: - r = frames[i + 1/*R*/]; - g = frames[i + 2/*G*/]; - b = frames[i + 3/*B*/]; - break; - default: - r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/); - g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); - b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); + return; + } + + const i = Timeline.search(frames, time, 4/*ENTRIES*/); + const curveType = this.curves[i >> 2]; + switch (curveType) { + case 0/*LINEAR*/: { + const before = frames[i]; + r = frames[i + 1/*R*/]; + g = frames[i + 2/*G*/]; + b = frames[i + 3/*B*/]; + const t = (time - before) / (frames[i + 4/*ENTRIES*/] - before); + r += (frames[i + 4/*ENTRIES*/ + 1/*R*/] - r) * t; + g += (frames[i + 4/*ENTRIES*/ + 2/*G*/] - g) * t; + b += (frames[i + 4/*ENTRIES*/ + 3/*B*/] - b) * t; + break; } - if (alpha !== 1) { - if (blend === MixBlend.setup) { - const setup = slot.data.setup.color; - r = setup.r + (r - setup.r) * alpha; - g = setup.g + (g - setup.g) * alpha; - b = setup.b + (b - setup.b) * alpha; - } else { - r = color.r + (r - color.r) * alpha; - g = color.g + (g - color.g) * alpha; - b = color.b + (b - color.b) * alpha; - } + case 1/*STEPPED*/: + r = frames[i + 1/*R*/]; + g = frames[i + 2/*G*/]; + b = frames[i + 3/*B*/]; + break; + default: + r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/); + g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); + b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); + } + if (alpha !== 1) { + if (fromSetup) { + const setup = slot.data.setupPose.color; + r = setup.r + (r - setup.r) * alpha; + g = setup.g + (g - setup.g) * alpha; + b = setup.b + (b - setup.b) * alpha; + } else { + r = color.r + (r - color.r) * alpha; + g = color.g + (g - color.g) * alpha; + b = color.b + (b - color.b) * alpha; } } color.r = r < 0 ? 0 : (r > 1 ? 1 : r); @@ -1071,7 +993,7 @@ export class RGBTimeline extends SlotCurveTimeline { } } -/** Changes the alpha for a slot's {@link SlotPose.color}. */ +/** Changes alpha for a slot's {@link SlotPose.color}. */ export class AlphaTimeline extends CurveTimeline1 implements SlotTimeline { slotIndex = 0; @@ -1080,42 +1002,33 @@ export class AlphaTimeline extends CurveTimeline1 implements SlotTimeline { this.slotIndex = slotIndex; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { const slot = skeleton.slots[this.slotIndex]; if (!slot.bone.active) return; - const color = (appliedPose ? slot.applied : slot.pose).color; + const color = (appliedPose ? slot.appliedPose : slot.pose).color; let a = 0; const frames = this.frames; if (time < frames[0]) { - const setup = slot.data.setup.color; - switch (blend) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime - case MixBlend.setup: - color.a = setup.a; - // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime - default: - return; - case MixBlend.first: a = color.a + (setup.a - color.a) * alpha; break; + if (fromSetup) color.a = slot.data.setupPose.color.a; + return; + } - } - } else { - a = this.getCurveValue(time); - if (alpha !== 1) { - if (blend === MixBlend.setup) { - const setup = slot.data.setup.color; - a = setup.a + (a - setup.a) * alpha; - } else - a = color.a + (a - color.a) * alpha; - } + a = this.getCurveValue(time); + if (alpha !== 1) { + if (fromSetup) { + const setup = slot.data.setupPose.color; + a = setup.a + (a - setup.a) * alpha; + } else + a = color.a + (a - color.a) * alpha; } color.a = a < 0 ? 0 : (a > 1 ? 1 : a); } } -/** Changes a slot's {@link SlotPose.color} and {@link SlotPose.darkColor} for two color tinting. */ +/** Changes {@link SlotPose.color} and {@link SlotPose.darkColor} for two color tinting. */ export class RGBA2Timeline extends SlotCurveTimeline { constructor (frameCount: number, bezierCount: number, slotIndex: number) { super(frameCount, bezierCount, slotIndex, // @@ -1141,94 +1054,83 @@ export class RGBA2Timeline extends SlotCurveTimeline { this.frames[frame + 7/*B2*/] = b2; } - protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, fromSetup: boolean, add: boolean) { // biome-ignore lint/style/noNonNullAssertion: reference runtime const light = pose.color, dark = pose.darkColor!; let r2 = 0, g2 = 0, b2 = 0 const frames = this.frames; if (time < frames[0]) { - const setup = slot.data.setup; - // biome-ignore lint/style/noNonNullAssertion: reference runtime - const setupLight = setup.color, setupDark = setup.darkColor!; - switch (blend) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime - case MixBlend.setup: - light.setFromColor(setupLight); - dark.r = setupDark.r; - dark.g = setupDark.g; - dark.b = setupDark.b; - // Fall through. - // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime - default: - return; - case MixBlend.first: - light.add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha, - (setupLight.a - light.a) * alpha); - r2 = dark.r + (setupDark.r - dark.r) * alpha; - g2 = dark.g + (setupDark.g - dark.g) * alpha; - b2 = dark.b + (setupDark.b - dark.b) * alpha; - } - } else { - let r = 0, g = 0, b = 0, a = 0; - const i = Timeline.search(frames, time, 8/*ENTRIES*/); - const curveType = this.curves[i >> 3]; - switch (curveType) { - case 0/*LINEAR*/: { - const before = frames[i]; - r = frames[i + 1/*R*/]; - g = frames[i + 2/*G*/]; - b = frames[i + 3/*B*/]; - a = frames[i + 4/*A*/]; - r2 = frames[i + 5/*R2*/]; - g2 = frames[i + 6/*G2*/]; - b2 = frames[i + 7/*B2*/]; - const t = (time - before) / (frames[i + 8/*ENTRIES*/] - before); - r += (frames[i + 8/*ENTRIES*/ + 1/*R*/] - r) * t; - g += (frames[i + 8/*ENTRIES*/ + 2/*G*/] - g) * t; - b += (frames[i + 8/*ENTRIES*/ + 3/*B*/] - b) * t; - a += (frames[i + 8/*ENTRIES*/ + 4/*A*/] - a) * t; - r2 += (frames[i + 8/*ENTRIES*/ + 5/*R2*/] - r2) * t; - g2 += (frames[i + 8/*ENTRIES*/ + 6/*G2*/] - g2) * t; - b2 += (frames[i + 8/*ENTRIES*/ + 7/*B2*/] - b2) * t; - break; - } - case 1/*STEPPED*/: - r = frames[i + 1/*R*/]; - g = frames[i + 2/*G*/]; - b = frames[i + 3/*B*/]; - a = frames[i + 4/*A*/]; - r2 = frames[i + 5/*R2*/]; - g2 = frames[i + 6/*G2*/]; - b2 = frames[i + 7/*B2*/]; - break; - default: - r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/); - g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); - b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); - a = this.getBezierValue(time, i, 4/*A*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/); - r2 = this.getBezierValue(time, i, 5/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/); - g2 = this.getBezierValue(time, i, 6/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/); - b2 = this.getBezierValue(time, i, 7/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 6 - 2/*BEZIER*/); - } - - if (alpha === 1) - light.set(r, g, b, a); - else if (blend === MixBlend.setup) { - const setupPose = slot.data.setup; - let setup = setupPose.color; - light.set(setup.r + (r - setup.r) * alpha, setup.g + (g - setup.g) * alpha, setup.b + (b - setup.b) * alpha, - setup.a + (a - setup.a) * alpha); + if (fromSetup) { + const setup = slot.data.setupPose; + light.setFromColor(setup.color); // biome-ignore lint/style/noNonNullAssertion: reference runtime - setup = setupPose.darkColor!; - r2 = setup.r + (r2 - setup.r) * alpha; - g2 = setup.g + (g2 - setup.g) * alpha; - b2 = setup.b + (b2 - setup.b) * alpha; - } else { - light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha); - r2 = dark.r + (r2 - dark.r) * alpha; - g2 = dark.g + (g2 - dark.g) * alpha; - b2 = dark.b + (b2 - dark.b) * alpha; + const setupDark = setup.darkColor!; + dark.r = setupDark.r; + dark.g = setupDark.g; + dark.b = setupDark.b; } + return; + } + + let r = 0, g = 0, b = 0, a = 0; + const i = Timeline.search(frames, time, 8/*ENTRIES*/); + const curveType = this.curves[i >> 3]; + switch (curveType) { + case 0/*LINEAR*/: { + const before = frames[i]; + r = frames[i + 1/*R*/]; + g = frames[i + 2/*G*/]; + b = frames[i + 3/*B*/]; + a = frames[i + 4/*A*/]; + r2 = frames[i + 5/*R2*/]; + g2 = frames[i + 6/*G2*/]; + b2 = frames[i + 7/*B2*/]; + const t = (time - before) / (frames[i + 8/*ENTRIES*/] - before); + r += (frames[i + 8/*ENTRIES*/ + 1/*R*/] - r) * t; + g += (frames[i + 8/*ENTRIES*/ + 2/*G*/] - g) * t; + b += (frames[i + 8/*ENTRIES*/ + 3/*B*/] - b) * t; + a += (frames[i + 8/*ENTRIES*/ + 4/*A*/] - a) * t; + r2 += (frames[i + 8/*ENTRIES*/ + 5/*R2*/] - r2) * t; + g2 += (frames[i + 8/*ENTRIES*/ + 6/*G2*/] - g2) * t; + b2 += (frames[i + 8/*ENTRIES*/ + 7/*B2*/] - b2) * t; + break; + } + case 1/*STEPPED*/: + r = frames[i + 1/*R*/]; + g = frames[i + 2/*G*/]; + b = frames[i + 3/*B*/]; + a = frames[i + 4/*A*/]; + r2 = frames[i + 5/*R2*/]; + g2 = frames[i + 6/*G2*/]; + b2 = frames[i + 7/*B2*/]; + break; + default: + r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/); + g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); + b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); + a = this.getBezierValue(time, i, 4/*A*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/); + r2 = this.getBezierValue(time, i, 5/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/); + g2 = this.getBezierValue(time, i, 6/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/); + b2 = this.getBezierValue(time, i, 7/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 6 - 2/*BEZIER*/); + } + + if (alpha === 1) + light.set(r, g, b, a); + else if (fromSetup) { + const setupPose = slot.data.setupPose; + let setup = setupPose.color; + light.set(setup.r + (r - setup.r) * alpha, setup.g + (g - setup.g) * alpha, setup.b + (b - setup.b) * alpha, + setup.a + (a - setup.a) * alpha); + // biome-ignore lint/style/noNonNullAssertion: reference runtime + setup = setupPose.darkColor!; + r2 = setup.r + (r2 - setup.r) * alpha; + g2 = setup.g + (g2 - setup.g) * alpha; + b2 = setup.b + (b2 - setup.b) * alpha; + } else { + light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha); + r2 = dark.r + (r2 - dark.r) * alpha; + g2 = dark.g + (g2 - dark.g) * alpha; + b2 = dark.b + (b2 - dark.b) * alpha; } dark.r = r2 < 0 ? 0 : (r2 > 1 ? 1 : r2); dark.g = g2 < 0 ? 0 : (g2 > 1 ? 1 : g2); @@ -1236,7 +1138,7 @@ export class RGBA2Timeline extends SlotCurveTimeline { } } -/** Changes a slot's {@link SlotPose.color} and {@link SlotPose.darkColor} for two color tinting. */ +/** Changes {@link SlotPose.color} and {@link SlotPose.darkColor} for two color tinting. */ export class RGB2Timeline extends SlotCurveTimeline { constructor (frameCount: number, bezierCount: number, slotIndex: number) { super(frameCount, bezierCount, slotIndex, // @@ -1260,94 +1162,82 @@ export class RGB2Timeline extends SlotCurveTimeline { this.frames[frame + 6/*B2*/] = b2; } - protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, fromSetup: boolean, add: boolean) { // biome-ignore lint/style/noNonNullAssertion: reference runtime const light = pose.color, dark = pose.darkColor!; let r = 0, g = 0, b = 0, r2 = 0, g2 = 0, b2 = 0 const frames = this.frames; if (time < frames[0]) { - const setup = slot.data.setup; - // biome-ignore lint/style/noNonNullAssertion: reference runtime - const setupLight = setup.color, setupDark = setup.darkColor!; - switch (blend) { - // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime - case MixBlend.setup: - light.r = setupLight.r; - light.g = setupLight.g; - light.b = setupLight.b; - dark.r = setupDark.r; - dark.g = setupDark.g; - dark.b = setupDark.b; - // Fall through. - // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime - default: - return; - case MixBlend.first: - r = light.r + (setupLight.r - light.r) * alpha; - g = light.g + (setupLight.g - light.g) * alpha; - b = light.b + (setupLight.b - light.b) * alpha; - r2 = dark.r + (setupDark.r - dark.r) * alpha; - g2 = dark.g + (setupDark.g - dark.g) * alpha; - b2 = dark.b + (setupDark.b - dark.b) * alpha; - } - } else { - const i = Timeline.search(frames, time, 7/*ENTRIES*/); - const curveType = this.curves[i / 7/*ENTRIES*/]; - switch (curveType) { - case 0/*LINEAR*/: { - const before = frames[i]; - r = frames[i + 1/*R*/]; - g = frames[i + 2/*G*/]; - b = frames[i + 3/*B*/]; - r2 = frames[i + 4/*R2*/]; - g2 = frames[i + 5/*G2*/]; - b2 = frames[i + 6/*B2*/]; - const t = (time - before) / (frames[i + 7/*ENTRIES*/] - before); - r += (frames[i + 7/*ENTRIES*/ + 1/*R*/] - r) * t; - g += (frames[i + 7/*ENTRIES*/ + 2/*G*/] - g) * t; - b += (frames[i + 7/*ENTRIES*/ + 3/*B*/] - b) * t; - r2 += (frames[i + 7/*ENTRIES*/ + 4/*R2*/] - r2) * t; - g2 += (frames[i + 7/*ENTRIES*/ + 5/*G2*/] - g2) * t; - b2 += (frames[i + 7/*ENTRIES*/ + 6/*B2*/] - b2) * t; - break; - } - case 1/*STEPPED*/: - r = frames[i + 1/*R*/]; - g = frames[i + 2/*G*/]; - b = frames[i + 3/*B*/]; - r2 = frames[i + 4/*R2*/]; - g2 = frames[i + 5/*G2*/]; - b2 = frames[i + 6/*B2*/]; - break; - default: - r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/); - g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); - b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); - r2 = this.getBezierValue(time, i, 4/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/); - g2 = this.getBezierValue(time, i, 5/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/); - b2 = this.getBezierValue(time, i, 6/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/); + if (fromSetup) { + const setup = slot.data.setupPose; + // biome-ignore lint/style/noNonNullAssertion: reference runtime + const setupLight = setup.color, setupDark = setup.darkColor!; + light.r = setupLight.r; + light.g = setupLight.g; + light.b = setupLight.b; + dark.r = setupDark.r; + dark.g = setupDark.g; + dark.b = setupDark.b; } + return; + } - if (alpha !== 1) { - if (blend === MixBlend.setup) { - const setupPose = slot.data.setup; - let setup = setupPose.color; - r = setup.r + (r - setup.r) * alpha; - g = setup.g + (g - setup.g) * alpha; - b = setup.b + (b - setup.b) * alpha; - // biome-ignore lint/style/noNonNullAssertion: reference runtime - setup = setupPose.darkColor!; - r2 = setup.r + (r2 - setup.r) * alpha; - g2 = setup.g + (g2 - setup.g) * alpha; - b2 = setup.b + (b2 - setup.b) * alpha; - } else { - r = light.r + (r - light.r) * alpha; - g = light.g + (g - light.g) * alpha; - b = light.b + (b - light.b) * alpha; - r2 = dark.r + (r2 - dark.r) * alpha; - g2 = dark.g + (g2 - dark.g) * alpha; - b2 = dark.b + (b2 - dark.b) * alpha; - } + const i = Timeline.search(frames, time, 7/*ENTRIES*/); + const curveType = this.curves[i / 7/*ENTRIES*/]; + switch (curveType) { + case 0/*LINEAR*/: { + const before = frames[i]; + r = frames[i + 1/*R*/]; + g = frames[i + 2/*G*/]; + b = frames[i + 3/*B*/]; + r2 = frames[i + 4/*R2*/]; + g2 = frames[i + 5/*G2*/]; + b2 = frames[i + 6/*B2*/]; + const t = (time - before) / (frames[i + 7/*ENTRIES*/] - before); + r += (frames[i + 7/*ENTRIES*/ + 1/*R*/] - r) * t; + g += (frames[i + 7/*ENTRIES*/ + 2/*G*/] - g) * t; + b += (frames[i + 7/*ENTRIES*/ + 3/*B*/] - b) * t; + r2 += (frames[i + 7/*ENTRIES*/ + 4/*R2*/] - r2) * t; + g2 += (frames[i + 7/*ENTRIES*/ + 5/*G2*/] - g2) * t; + b2 += (frames[i + 7/*ENTRIES*/ + 6/*B2*/] - b2) * t; + break; + } + case 1/*STEPPED*/: + r = frames[i + 1/*R*/]; + g = frames[i + 2/*G*/]; + b = frames[i + 3/*B*/]; + r2 = frames[i + 4/*R2*/]; + g2 = frames[i + 5/*G2*/]; + b2 = frames[i + 6/*B2*/]; + break; + default: + r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/); + g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); + b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); + r2 = this.getBezierValue(time, i, 4/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/); + g2 = this.getBezierValue(time, i, 5/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/); + b2 = this.getBezierValue(time, i, 6/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/); + } + + if (alpha !== 1) { + if (fromSetup) { + const setupPose = slot.data.setupPose; + let setup = setupPose.color; + r = setup.r + (r - setup.r) * alpha; + g = setup.g + (g - setup.g) * alpha; + b = setup.b + (b - setup.b) * alpha; + // biome-ignore lint/style/noNonNullAssertion: reference runtime + setup = setupPose.darkColor!; + r2 = setup.r + (r2 - setup.r) * alpha; + g2 = setup.g + (g2 - setup.g) * alpha; + b2 = setup.b + (b2 - setup.b) * alpha; + } else { + r = light.r + (r - light.r) * alpha; + g = light.g + (g - light.g) * alpha; + b = light.b + (b - light.b) * alpha; + r2 = dark.r + (r2 - dark.r) * alpha; + g2 = dark.g + (g2 - dark.g) * alpha; + b2 = dark.b + (b2 - dark.b) * alpha; } } light.r = r < 0 ? 0 : (r > 1 ? 1 : r); @@ -1359,7 +1249,7 @@ export class RGB2Timeline extends SlotCurveTimeline { } } -/** Changes a slot's {@link SlotPose.ttachment}. */ +/** Changes {@link SlotPose.ttachment}. */ export class AttachmentTimeline extends Timeline implements SlotTimeline { slotIndex = 0; @@ -1370,6 +1260,7 @@ export class AttachmentTimeline extends Timeline implements SlotTimeline { super(frameCount, `${Property.attachment}|${slotIndex}`); this.slotIndex = slotIndex; this.attachmentNames = new Array(frameCount); + this.instant = true; } getFrameCount () { @@ -1382,17 +1273,15 @@ export class AttachmentTimeline extends Timeline implements SlotTimeline { this.attachmentNames[frame] = attachmentName; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { const slot = skeleton.slots[this.slotIndex]; if (!slot.bone.active) return; - const pose = appliedPose ? slot.applied : slot.pose; + const pose = appliedPose ? slot.appliedPose : slot.pose; - if (direction === MixDirection.out) { - if (blend === MixBlend.setup) this.setAttachment(skeleton, pose, slot.data.attachmentName); - } else if (time < this.frames[0]) { - if (blend === MixBlend.setup || blend === MixBlend.first) this.setAttachment(skeleton, pose, slot.data.attachmentName); + if (out || time < this.frames[0]) { + if (fromSetup) this.setAttachment(skeleton, pose, slot.data.attachmentName); } else this.setAttachment(skeleton, pose, this.attachmentNames[Timeline.search(this.frames, time)]); } @@ -1402,7 +1291,7 @@ export class AttachmentTimeline extends Timeline implements SlotTimeline { } } -/** Changes a slot's {@link SlotPose.deform} to deform a {@link VertexAttachment}. */ +/** Changes {@link SlotPose.deform} to deform a {@link VertexAttachment}. */ export class DeformTimeline extends SlotCurveTimeline { /** The attachment that will be deformed. * @@ -1416,6 +1305,7 @@ export class DeformTimeline extends SlotCurveTimeline { super(frameCount, bezierCount, slotIndex, `${Property.deform}|${slotIndex}|${attachment.id}`); this.attachment = attachment; this.vertices = new Array(frameCount); + this.additive = true; } getFrameCount () { @@ -1482,39 +1372,20 @@ export class DeformTimeline extends SlotCurveTimeline { return y + (1 - y) * (time - x) / (this.frames[frame + this.getFrameEntries()] - x); } - protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, fromSetup: boolean, add: boolean) { if (!(pose.attachment instanceof VertexAttachment)) return; const vertexAttachment = pose.attachment; if (vertexAttachment.timelineAttachment !== this.attachment) return; const deform = pose.deform; - if (deform.length === 0) blend = MixBlend.setup; + if (deform.length === 0) fromSetup = true; const vertices = this.vertices; const vertexCount = vertices[0].length; const frames = this.frames; if (time < frames[0]) { - switch (blend) { - case MixBlend.setup: - deform.length = 0; - return; - case MixBlend.first: - if (alpha === 1) { - deform.length = 0; - return; - } - deform.length = vertexCount; - if (!vertexAttachment.bones) { // Unweighted vertex positions. - const setupVertices = vertexAttachment.vertices; - for (let i = 0; i < vertexCount; i++) - deform[i] += (setupVertices[i] - deform[i]) * alpha; - } else { // Weighted deform offsets. - alpha = 1 - alpha; - for (let i = 0; i < vertexCount; i++) - deform[i] *= alpha; - } - } + if (fromSetup) deform.length = 0; return; } @@ -1522,7 +1393,7 @@ export class DeformTimeline extends SlotCurveTimeline { if (time >= frames[frames.length - 1]) { // Time is after last frame. const lastVertices = vertices[frames.length - 1]; if (alpha === 1) { - if (blend === MixBlend.add) { + if (add && !fromSetup) { if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. const setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) @@ -1533,36 +1404,29 @@ export class DeformTimeline extends SlotCurveTimeline { } } else // Vertex positions or deform offsets, no alpha. Utils.arrayCopy(lastVertices, 0, deform, 0, vertexCount); - } else { - switch (blend) { - case MixBlend.setup: { - if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. - const setupVertices = vertexAttachment.vertices; - for (let i = 0; i < vertexCount; i++) { - const setup = setupVertices[i]; - deform[i] = setup + (lastVertices[i] - setup) * alpha; - } - } else { // Weighted deform offsets, with alpha. - for (let i = 0; i < vertexCount; i++) - deform[i] = lastVertices[i] * alpha; - } - break; + } else if (fromSetup) { + if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const setup = setupVertices[i]; + deform[i] = setup + (lastVertices[i] - setup) * alpha; } - case MixBlend.first: - case MixBlend.replace: // Vertex positions or deform offsets, with alpha. - for (let i = 0; i < vertexCount; i++) - deform[i] += (lastVertices[i] - deform[i]) * alpha; - break; - case MixBlend.add: - if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. - const setupVertices = vertexAttachment.vertices; - for (let i = 0; i < vertexCount; i++) - deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; - } else { // Weighted deform offsets, alpha. - for (let i = 0; i < vertexCount; i++) - deform[i] += lastVertices[i] * alpha; - } + } else { // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) + deform[i] = lastVertices[i] * alpha; } + } else if (add) { + if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) + deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; + } else { // Weighted deform offsets, alpha. + for (let i = 0; i < vertexCount; i++) + deform[i] += lastVertices[i] * alpha; + } + } else { // Vertex positions or deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) + deform[i] += (lastVertices[i] - deform[i]) * alpha; } return; } @@ -1573,7 +1437,7 @@ export class DeformTimeline extends SlotCurveTimeline { const nextVertices = vertices[frame + 1]; if (alpha === 1) { - if (blend === MixBlend.add) { + if (add && !fromSetup) { if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. const setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) { @@ -1594,49 +1458,42 @@ export class DeformTimeline extends SlotCurveTimeline { deform[i] = prev + (nextVertices[i] - prev) * percent; } } - } else { - switch (blend) { - case MixBlend.setup: { - if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. - const setupVertices = vertexAttachment.vertices; - for (let i = 0; i < vertexCount; i++) { - const prev = prevVertices[i], setup = setupVertices[i]; - deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; - } - } else { // Weighted deform offsets, with alpha. - for (let i = 0; i < vertexCount; i++) { - const prev = prevVertices[i]; - deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; - } - } - break; + } else if (fromSetup) { + if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i], setup = setupVertices[i]; + deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; } - case MixBlend.first: - case MixBlend.replace: // Vertex positions or deform offsets, with alpha. - for (let i = 0; i < vertexCount; i++) { - const prev = prevVertices[i]; - deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; - } - break; - case MixBlend.add: - if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. - const setupVertices = vertexAttachment.vertices; - for (let i = 0; i < vertexCount; i++) { - const prev = prevVertices[i]; - deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; - } - } else { // Weighted deform offsets, with alpha. - for (let i = 0; i < vertexCount; i++) { - const prev = prevVertices[i]; - deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; - } - } + } else { // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + } else if (add) { + if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. + const setupVertices = vertexAttachment.vertices; + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; + } + } else { // Weighted deform offsets, with alpha. + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; + } + } + } else { + for (let i = 0; i < vertexCount; i++) { + const prev = prevVertices[i]; + deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; } } } } -/** Changes a slot's {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */ +/** Changes {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */ export class SequenceTimeline extends Timeline implements SlotTimeline { static ENTRIES = 3; static MODE = 1; @@ -1650,6 +1507,7 @@ export class SequenceTimeline extends Timeline implements SlotTimeline { super(frameCount, `${Property.sequence}|${slotIndex}|${attachment.sequence!.id}`); this.slotIndex = slotIndex; this.attachment = attachment; + this.instant = true; } getFrameEntries () { @@ -1678,26 +1536,26 @@ export class SequenceTimeline extends Timeline implements SlotTimeline { frames[frame + SequenceTimeline.DELAY] = delay; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { const slot = skeleton.slots[this.slotIndex]; if (!slot.bone.active) return; - const pose = appliedPose ? slot.applied : slot.pose; + const pose = appliedPose ? slot.appliedPose : slot.pose; const slotAttachment = pose.attachment as Attachment; const attachment = this.attachment as unknown as Attachment; if (!(isHasSequence(slotAttachment)) || slotAttachment.timelineAttachment !== attachment) return; - if (direction === MixDirection.out) { - if (blend === MixBlend.setup) pose.sequenceIndex = -1; + if (out) { + if (fromSetup) pose.sequenceIndex = -1; return; } const frames = this.frames; if (time < frames[0]) { - if (blend === MixBlend.setup || blend === MixBlend.first) pose.sequenceIndex = -1; + if (fromSetup) pose.sequenceIndex = -1; return; } @@ -1741,8 +1599,8 @@ export class EventTimeline extends Timeline { constructor (frameCount: number) { super(frameCount, ...EventTimeline.propertyIds); - this.events = new Array(frameCount); + this.instant = true; } getFrameCount () { @@ -1756,8 +1614,8 @@ export class EventTimeline extends Timeline { } /** Fires events for frames > `lastTime` and <= `time`. */ - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, - blend: MixBlend, direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton | null, lastTime: number, time: number, firedEvents: Array, alpha: number, + fromSetup: boolean, add: boolean, out: boolean, appliedPose: boolean) { if (!firedEvents) return; @@ -1765,7 +1623,7 @@ export class EventTimeline extends Timeline { const frameCount = this.frames.length; if (lastTime > time) { // Apply after lastTime for looped animations. - this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha, blend, direction, appliedPose); + this.apply(null, lastTime, Number.MAX_VALUE, firedEvents, 0, false, false, false, false); lastTime = -1; } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. return; @@ -1787,7 +1645,7 @@ export class EventTimeline extends Timeline { } } -/** Changes a skeleton's {@link Skeleton#drawOrder}. */ +/** Changes the {@link Skeleton#getDrawOrder()}. */ export class DrawOrderTimeline extends Timeline { static readonly propertyID = `${Property.drawOrder}`; static propertyIds = [DrawOrderTimeline.propertyID]; @@ -1798,6 +1656,7 @@ export class DrawOrderTimeline extends Timeline { constructor (frameCount: number) { super(frameCount, ...DrawOrderTimeline.propertyIds); this.drawOrders = new Array | null>(frameCount); + this.instant = true; } getFrameCount () { @@ -1813,32 +1672,25 @@ export class DrawOrderTimeline extends Timeline { } apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, - blend: MixBlend, direction: MixDirection, appliedPose: boolean) { - - if (direction === MixDirection.out) { - if (blend === MixBlend.setup) Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); + fromSetup: boolean, add: boolean, out: boolean, appliedPose: boolean) { + const pose = (appliedPose ? skeleton.drawOrder.appliedPose : skeleton.drawOrder.pose); + const setup = skeleton.slots; + if (out || time < this.frames[0]) { + if (fromSetup) Utils.arrayCopy(setup, 0, pose, 0, skeleton.slots.length); return; } - if (time < this.frames[0]) { - if (blend === MixBlend.setup || blend === MixBlend.first) Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); - return; - } - - const idx = Timeline.search(this.frames, time); - const drawOrderToSetupIndex = this.drawOrders[idx]; - if (!drawOrderToSetupIndex) - Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); + const order = this.drawOrders[Timeline.search(this.frames, time)]; + if (!order) + Utils.arrayCopy(setup, 0, pose, 0, skeleton.slots.length); else { - const drawOrder: Array = skeleton.drawOrder; - const slots: Array = skeleton.slots; - for (let i = 0, n = drawOrderToSetupIndex.length; i < n; i++) - drawOrder[i] = slots[drawOrderToSetupIndex[i]]; + for (let i = 0, n = order.length; i < n; i++) + pose[i] = setup[order[i]]; } } } -/** Changes a subset of a skeleton's {@link Skeleton#getDrawOrder()}. */ +/** Changes a subset of the {@link Skeleton#getDrawOrder() draw order}. */ export class DrawOrderFolderTimeline extends Timeline { private readonly slots: number[]; private readonly inFolder: boolean[]; @@ -1853,6 +1705,7 @@ export class DrawOrderFolderTimeline extends Timeline { this.inFolder = new Array(slotCount); for (const i of slots) this.inFolder[i] = true; + this.instant = true; } private static propertyIds (slots: number[]): string[] { @@ -1886,39 +1739,34 @@ export class DrawOrderFolderTimeline extends Timeline { this.drawOrders[frame] = drawOrder; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean): void { - - if (direction === MixDirection.out) { - if (blend === MixBlend.setup) this.setup(skeleton); - } else if (time < this.frames[0]) { - if (blend === MixBlend.setup || blend === MixBlend.first) this.setup(skeleton); + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean): void { + const pose = (appliedPose ? skeleton.drawOrder.appliedPose : skeleton.drawOrder.pose); + const setup = skeleton.slots; + if (out || time < this.frames[0]) { + if (fromSetup) this.setup(pose, setup); } else { const order = this.drawOrders[Timeline.search(this.frames, time)]; if (!order) - this.setup(skeleton); - else - this.apply1(skeleton, order); - } - } - - private setup (skeleton: Skeleton): void { - const { inFolder, slots } = this; - const { drawOrder, slots: allSlots } = skeleton; - for (let i = 0, found = 0, done = slots.length; ; i++) { - if (inFolder[drawOrder[i].data.index]) { - drawOrder[i] = allSlots[slots[found]]; - if (++found === done) break; + this.setup(pose, setup); + else { + const inFolder = this.inFolder; + const slots = this.slots; + for (let i = 0, found = 0, done = slots.length; ; i++) { + if (inFolder[pose[i].data.index]) { + pose[i] = setup[slots[order[found]]]; + if (++found === done) break; + } + } } } } - private apply1 (skeleton: Skeleton, order: number[]): void { + private setup (pose: Slot[], setup: Slot[]): void { const { inFolder, slots } = this; - const { drawOrder, slots: allSlots } = skeleton; for (let i = 0, found = 0, done = slots.length; ; i++) { - if (inFolder[drawOrder[i].data.index]) { - drawOrder[i] = allSlots[slots[order[found]]]; + if (inFolder[pose[i].data.index]) { + pose[i] = setup[slots[found]]; if (++found === done) break; } } @@ -1926,8 +1774,8 @@ export class DrawOrderFolderTimeline extends Timeline { } export interface ConstraintTimeline { - /** The index of the constraint in {@link Skeleton.constraints} that will be changed when this timeline is applied, or - * -1 if a specific constraint will not be changed. */ + /** The index of the constraint in {@link Skeleton#constraints} that will be changed when this timeline is applied, or -1 if + * a specific constraint will not be changed. */ readonly constraintIndex: number; } @@ -1935,7 +1783,7 @@ export function isConstraintTimeline (obj: Timeline & Partial, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as IkConstraint; if (!constraint.active) return; - const pose = appliedPose ? constraint.applied : constraint.pose; + const pose = appliedPose ? constraint.appliedPose : constraint.pose; const frames = this.frames; if (time < frames[0]) { - const setup = constraint.data.setup; - switch (blend) { - case MixBlend.setup: - pose.mix = setup.mix; - pose.softness = setup.softness; - pose.bendDirection = setup.bendDirection; - pose.compress = setup.compress; - pose.stretch = setup.stretch; - return; - case MixBlend.first: - pose.mix += (setup.mix - pose.mix) * alpha; - pose.softness += (setup.softness - pose.softness) * alpha; - pose.bendDirection = setup.bendDirection; - pose.compress = setup.compress; - pose.stretch = setup.stretch; + if (fromSetup) { + const setup = constraint.data.setupPose; + pose.mix = setup.mix; + pose.softness = setup.softness; + pose.bendDirection = setup.bendDirection; + pose.compress = setup.compress; + pose.stretch = setup.stretch; } return; } @@ -2014,28 +1854,24 @@ export class IkConstraintTimeline extends CurveTimeline implements ConstraintTim softness = this.getBezierValue(time, i, 2/*SOFTNESS*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); } - if (blend === MixBlend.setup) { - const setup = constraint.data.setup; - pose.mix = setup.mix + (mix - setup.mix) * alpha; - pose.softness = setup.softness + (softness - setup.softness) * alpha; - if (direction === MixDirection.out) { - pose.bendDirection = setup.bendDirection; - pose.compress = setup.compress; - pose.stretch = setup.stretch; - return; + const base = fromSetup ? constraint.data.setupPose : pose; + pose.mix = base.mix + (mix - base.mix) * alpha; + pose.softness = base.softness + (softness - base.softness) * alpha; + if (out) { + if (fromSetup) { + pose.bendDirection = base.bendDirection; + pose.compress = base.compress; + pose.stretch = base.stretch; } } else { - pose.mix += (mix - pose.mix) * alpha; - pose.softness += (softness - pose.softness) * alpha; - if (direction === MixDirection.out) return; + pose.bendDirection = frames[i + 3/*BEND_DIRECTION*/]; + pose.compress = frames[i + 4/*COMPRESS*/] !== 0; + pose.stretch = frames[i + 5/*STRETCH*/] !== 0; } - pose.bendDirection = frames[i + 3/*BEND_DIRECTION*/]; - pose.compress = frames[i + 4/*COMPRESS*/] !== 0; - pose.stretch = frames[i + 5/*STRETCH*/] !== 0; } } -/** Changes a transform constraint's {@link TransformConstraintPose.mixRotate}, {@link TransformConstraintPose.mixX}, +/** Changes {@link TransformConstraintPose.mixRotate}, {@link TransformConstraintPose.mixX}, * {@link TransformConstraintPose.mixY}, {@link TransformConstraintPose.mixScaleX}, * {@link TransformConstraintPose.mixScaleY}, and {@link TransformConstraintPose.mixShearY}. */ export class TransformConstraintTimeline extends CurveTimeline implements ConstraintTimeline { @@ -2045,6 +1881,7 @@ export class TransformConstraintTimeline extends CurveTimeline implements Constr constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, `${Property.transformConstraint}|${constraintIndex}`); this.constraintIndex = constraintIndex; + this.additive = true; } getFrameEntries () { @@ -2067,32 +1904,23 @@ export class TransformConstraintTimeline extends CurveTimeline implements Constr frames[frame + 6/*SHEARY*/] = mixShearY; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as TransformConstraint; if (!constraint.active) return; - const pose = appliedPose ? constraint.applied : constraint.pose; + const pose = appliedPose ? constraint.appliedPose : constraint.pose; const frames = this.frames; if (time < frames[0]) { - const setup = constraint.data.setup; - switch (blend) { - case MixBlend.setup: - pose.mixRotate = setup.mixRotate; - pose.mixX = setup.mixX; - pose.mixY = setup.mixY; - pose.mixScaleX = setup.mixScaleX; - pose.mixScaleY = setup.mixScaleY; - pose.mixShearY = setup.mixShearY; - return; - case MixBlend.first: - pose.mixRotate += (setup.mixRotate - pose.mixRotate) * alpha; - pose.mixX += (setup.mixX - pose.mixX) * alpha; - pose.mixY += (setup.mixY - pose.mixY) * alpha; - pose.mixScaleX += (setup.mixScaleX - pose.mixScaleX) * alpha; - pose.mixScaleY += (setup.mixScaleY - pose.mixScaleY) * alpha; - pose.mixShearY += (setup.mixShearY - pose.mixShearY) * alpha; + if (fromSetup) { + const setup = constraint.data.setupPose; + pose.mixRotate = setup.mixRotate; + pose.mixX = setup.mixX; + pose.mixY = setup.mixY; + pose.mixScaleX = setup.mixScaleX; + pose.mixScaleY = setup.mixScaleY; + pose.mixShearY = setup.mixShearY; } return; } @@ -2135,38 +1963,26 @@ export class TransformConstraintTimeline extends CurveTimeline implements Constr shearY = this.getBezierValue(time, i, 6/*SHEARY*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/); } - switch (blend) { - case MixBlend.setup: { - const setup = constraint.data.setup; - pose.mixRotate = setup.mixRotate + (rotate - setup.mixRotate) * alpha; - pose.mixX = setup.mixX + (x - setup.mixX) * alpha; - pose.mixY = setup.mixY + (y - setup.mixY) * alpha; - pose.mixScaleX = setup.mixScaleX + (scaleX - setup.mixScaleX) * alpha; - pose.mixScaleY = setup.mixScaleY + (scaleY - setup.mixScaleY) * alpha; - pose.mixShearY = setup.mixShearY + (shearY - setup.mixShearY) * alpha; - break; - } - case MixBlend.first: - case MixBlend.replace: - pose.mixRotate += (rotate - pose.mixRotate) * alpha; - pose.mixX += (x - pose.mixX) * alpha; - pose.mixY += (y - pose.mixY) * alpha; - pose.mixScaleX += (scaleX - pose.mixScaleX) * alpha; - pose.mixScaleY += (scaleY - pose.mixScaleY) * alpha; - pose.mixShearY += (shearY - pose.mixShearY) * alpha; - break; - case MixBlend.add: - pose.mixRotate += rotate * alpha; - pose.mixX += x * alpha; - pose.mixY += y * alpha; - pose.mixScaleX += scaleX * alpha; - pose.mixScaleY += scaleY * alpha; - pose.mixShearY += shearY * alpha; - break; + const base = fromSetup ? constraint.data.setupPose : pose; + if (add) { + pose.mixRotate = base.mixRotate + rotate * alpha; + pose.mixX = base.mixX + x * alpha; + pose.mixY = base.mixY + y * alpha; + pose.mixScaleX = base.mixScaleX + scaleX * alpha; + pose.mixScaleY = base.mixScaleY + scaleY * alpha; + pose.mixShearY = base.mixShearY + shearY * alpha; + } else { + pose.mixRotate = base.mixRotate + (rotate - base.mixRotate) * alpha; + pose.mixX = base.mixX + (x - base.mixX) * alpha; + pose.mixY = base.mixY + (y - base.mixY) * alpha; + pose.mixScaleX = base.mixScaleX + (scaleX - base.mixScaleX) * alpha; + pose.mixScaleY = base.mixScaleY + (scaleY - base.mixScaleY) * alpha; + pose.mixShearY = base.mixShearY + (shearY - base.mixShearY) * alpha; } } } +/** The base class for timelines that change 1 constraint property with a curve. */ export abstract class ConstraintTimeline1 extends CurveTimeline1 implements ConstraintTimeline { readonly constraintIndex: number; @@ -2176,42 +1992,43 @@ export abstract class ConstraintTimeline1 extends CurveTimeline1 implements Cons } } -/** Changes a path constraint's {@link PathConstraintPose.position}. */ +/** Changes {@link PathConstraintPose.position}. */ export class PathConstraintPositionTimeline extends ConstraintTimeline1 { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.pathConstraintPosition); + this.additive = true; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as PathConstraint; if (constraint.active) { - const pose = appliedPose ? constraint.applied : constraint.pose; - pose.position = this.getAbsoluteValue(time, alpha, blend, pose.position, constraint.data.setup.position); + const pose = appliedPose ? constraint.appliedPose : constraint.pose; + pose.position = this.getAbsoluteValue(time, alpha, fromSetup, add, pose.position, constraint.data.setupPose.position); } } } -/** Changes a path constraint's {@link PathConstraintPose.spacing}. */ +/** Changes {@link PathConstraintPose.spacing}. */ export class PathConstraintSpacingTimeline extends ConstraintTimeline1 { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.pathConstraintSpacing); } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as PathConstraint; if (constraint.active) { - const pose = appliedPose ? constraint.applied : constraint.pose; - pose.spacing = this.getAbsoluteValue(time, alpha, blend === MixBlend.add ? MixBlend.replace : blend, pose.spacing, - constraint.data.setup.spacing); + const pose = appliedPose ? constraint.appliedPose : constraint.pose; + pose.spacing = this.getAbsoluteValue(time, alpha, fromSetup, false, pose.spacing, + constraint.data.setupPose.spacing); } } } -/** Changes a transform constraint's {@link PathConstraint.mixRotate()}, {@link PathConstraint.mixX()}, and +/** Changes {@link PathConstraint.mixRotate()}, {@link PathConstraint.mixX()}, and * {@link PathConstraint.mixY()}. */ export class PathConstraintMixTimeline extends CurveTimeline implements ConstraintTimeline { readonly constraintIndex: number; @@ -2219,6 +2036,7 @@ export class PathConstraintMixTimeline extends CurveTimeline implements Constrai constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, `${Property.pathConstraintMix}|${constraintIndex}`); this.constraintIndex = constraintIndex; + this.additive = true; } getFrameEntries () { @@ -2237,26 +2055,20 @@ export class PathConstraintMixTimeline extends CurveTimeline implements Constrai frames[frame + 3/*Y*/] = mixY; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as PathConstraint; if (!constraint.active) return; - const pose = appliedPose ? constraint.applied : constraint.pose; + const pose = appliedPose ? constraint.appliedPose : constraint.pose; const frames = this.frames; if (time < frames[0]) { - const setup = constraint.data.setup; - switch (blend) { - case MixBlend.setup: - pose.mixRotate = setup.mixRotate; - pose.mixX = setup.mixX; - pose.mixY = setup.mixY; - return; - case MixBlend.first: - pose.mixRotate += (setup.mixRotate - pose.mixRotate) * alpha; - pose.mixX += (setup.mixX - pose.mixX) * alpha; - pose.mixY += (setup.mixY - pose.mixY) * alpha; + if (fromSetup) { + const setup = constraint.data.setupPose; + pose.mixRotate = setup.mixRotate; + pose.mixX = setup.mixX; + pose.mixY = setup.mixY; } return; } @@ -2287,46 +2099,44 @@ export class PathConstraintMixTimeline extends CurveTimeline implements Constrai y = this.getBezierValue(time, i, 3/*Y*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); } - if (blend === MixBlend.setup) { - const setup = constraint.data.setup; - pose.mixRotate = setup.mixRotate + (rotate - setup.mixRotate) * alpha; - pose.mixX = setup.mixX + (x - setup.mixX) * alpha; - pose.mixY = setup.mixY + (y - setup.mixY) * alpha; + const base = fromSetup ? constraint.data.setupPose : pose; + if (add) { + pose.mixRotate = base.mixRotate + rotate * alpha; + pose.mixX = base.mixX + x * alpha; + pose.mixY = base.mixY + y * alpha; } else { - pose.mixRotate += (rotate - pose.mixRotate) * alpha; - pose.mixX += (x - pose.mixX) * alpha; - pose.mixY += (y - pose.mixY) * alpha; + pose.mixRotate = base.mixRotate + (rotate - base.mixRotate) * alpha; + pose.mixX = base.mixX + (x - base.mixX) * alpha; + pose.mixY = base.mixY + (y - base.mixY) * alpha; } } } /** The base class for most {@link PhysicsConstraint} timelines. */ export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 { - additive = false; - /** @param constraintIndex -1 for all physics constraints in the skeleton. */ constructor (frameCount: number, bezierCount: number, constraintIndex: number, property: number) { super(frameCount, bezierCount, constraintIndex, property); } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - if (blend === MixBlend.add && !this.additive) blend = MixBlend.replace; + if (add && !this.additive) add = false; if (this.constraintIndex === -1) { const value = time >= this.frames[0] ? this.getCurveValue(time) : 0; const constraints = skeleton.physics; for (const constraint of constraints) { if (constraint.active && this.global(constraint.data)) { - const pose = appliedPose ? constraint.applied : constraint.pose; - this.set(pose, this.getAbsoluteValue(time, alpha, blend, this.get(pose), this.get(constraint.data.setup), value)); + const pose = appliedPose ? constraint.appliedPose : constraint.pose; + this.set(pose, this.getAbsoluteValue(time, alpha, fromSetup, add, this.get(pose), this.get(constraint.data.setupPose), value)); } } } else { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as PhysicsConstraint; if (constraint.active) { - const pose = appliedPose ? constraint.applied : constraint.pose; - this.set(pose, this.getAbsoluteValue(time, alpha, blend, this.get(pose), this.get(constraint.data.setup))); + const pose = appliedPose ? constraint.appliedPose : constraint.pose; + this.set(pose, this.getAbsoluteValue(time, alpha, fromSetup, add, this.get(pose), this.get(constraint.data.setupPose))); } } } @@ -2338,7 +2148,7 @@ export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 { abstract global (constraint: PhysicsConstraintData): boolean; } -/** Changes a physics constraint's {@link PhysicsConstraintPose.inertia}. */ +/** Changes {@link PhysicsConstraintPose.inertia}. */ export class PhysicsConstraintInertiaTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintInertia); @@ -2357,7 +2167,7 @@ export class PhysicsConstraintInertiaTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraintPose.strength}. */ +/** Changes {@link PhysicsConstraintPose.strength}. */ export class PhysicsConstraintStrengthTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintStrength); @@ -2375,7 +2185,7 @@ export class PhysicsConstraintStrengthTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraintPose.damping}. */ +/** Changes {@link PhysicsConstraintPose.damping}. */ export class PhysicsConstraintDampingTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintDamping); @@ -2394,7 +2204,7 @@ export class PhysicsConstraintDampingTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraintPose.massInverse}. The timeline values are not inverted. */ +/** Changes {@link PhysicsConstraintPose.massInverse}. The timeline values are not inverted. */ export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintMass); @@ -2413,7 +2223,7 @@ export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline { } } -/** Changes a physics constraint's {@link PhysicsConstraintPose.wind}. */ +/** Changes {@link PhysicsConstraintPose.wind}. */ export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind); @@ -2433,7 +2243,7 @@ export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline { } } -/** Changes a physics constraint's {@link PhysicsConstraintPose.gravity}. */ +/** Changes {@link PhysicsConstraintPose.gravity}. */ export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity); @@ -2453,7 +2263,7 @@ export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraintPose.mix}. */ +/** Changes {@link PhysicsConstraintPose.mix}. */ export class PhysicsConstraintMixTimeline extends PhysicsConstraintTimeline { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintMix); @@ -2484,6 +2294,7 @@ export class PhysicsConstraintResetTimeline extends Timeline implements Constrai constructor (frameCount: number, constraintIndex: number) { super(frameCount, ...PhysicsConstraintResetTimeline.propertyIds); this.constraintIndex = constraintIndex; + this.instant = true; } getFrameCount () { @@ -2497,8 +2308,8 @@ export class PhysicsConstraintResetTimeline extends Timeline implements Constrai } /** Resets the physics constraint when frames > lastTime and <= time. */ - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { let constraint: PhysicsConstraint | undefined; if (this.constraintIndex !== -1) { @@ -2509,7 +2320,7 @@ export class PhysicsConstraintResetTimeline extends Timeline implements Constrai const frames = this.frames; if (lastTime > time) { // Apply after lastTime for looped animations. - this.apply(skeleton, lastTime, Number.MAX_VALUE, [], alpha, blend, direction, appliedPose); + this.apply(skeleton, lastTime, Number.MAX_VALUE, [], alpha, false, false, false, false); lastTime = -1; } else if (lastTime >= frames[frames.length - 1]) // Last time is after last frame. return; @@ -2527,36 +2338,38 @@ export class PhysicsConstraintResetTimeline extends Timeline implements Constrai } } -/** Changes a slider's {@link SliderPose.time()}. */ +/** Changes {@link SliderPose.time()}. */ export class SliderTimeline extends ConstraintTimeline1 { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.sliderTime); + this.additive = true; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as Slider; if (constraint.active) { - const pose = appliedPose ? constraint.applied : constraint.pose; - pose.time = this.getAbsoluteValue(time, alpha, blend, pose.time, constraint.data.setup.time); + const pose = appliedPose ? constraint.appliedPose : constraint.pose; + pose.time = this.getAbsoluteValue(time, alpha, fromSetup, add, pose.time, constraint.data.setupPose.time); } } } -/** Changes a slider's {@link SliderPose.mix()}. */ +/** Changes {@link SliderPose.mix()}. */ export class SliderMixTimeline extends ConstraintTimeline1 { constructor (frameCount: number, bezierCount: number, constraintIndex: number) { super(frameCount, bezierCount, constraintIndex, Property.sliderMix); + this.additive = true; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, - direction: MixDirection, appliedPose: boolean) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, fromSetup: boolean, + add: boolean, out: boolean, appliedPose: boolean) { - const constraint = skeleton.constraints[this.constraintIndex]; + const constraint = skeleton.constraints[this.constraintIndex] as Slider; if (constraint.active) { - const pose = appliedPose ? constraint.applied : constraint.pose; - pose.mix = this.getAbsoluteValue(time, alpha, blend, pose.mix, constraint.data.setup.mix); + const pose = appliedPose ? constraint.appliedPose : constraint.pose; + pose.mix = this.getAbsoluteValue(time, alpha, fromSetup, add, pose.mix, constraint.data.setupPose.mix); } } } diff --git a/spine-ts/spine-core/src/AnimationState.ts b/spine-ts/spine-core/src/AnimationState.ts index 9d80a8009..fb3099ef8 100644 --- a/spine-ts/spine-core/src/AnimationState.ts +++ b/spine-ts/spine-core/src/AnimationState.ts @@ -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("", [], 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(() => 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, 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. - *

+ * * 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 delay (ie the mix - * ends at (delay = 0) or before (delay < 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 Empty animations in the Spine + * See Empty animations 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 - * Empty animations 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 delay (ie the mix ends at (delay = 0) or - * before (delay < 0) the previous track entry duration). If the previous entry is looping, its next - * loop completion is used instead of its duration. + * Empty animations in the Spine Runtimes + * Guide. + * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry minus + * any mix duration plus the specified delay (ie the mix ends at (when delay = 0) or before + * (when delay < 0) the previous track entry duration). If the previous entry is looping, its next loop + * completion is used instead of its duration. * @return A track entry to allow further customization of animation playback. References to the track entry must not be kept * after the {@link AnimationStateListener#dispose(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 Empty animations in the Spine + * See Empty animations 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, mixingFrom 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, mixingTo 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 alphaAttachmentThreshold, 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 - * 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: 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: + * + *

+	 * entry.mixDuration = 0.25;
+ * entry.delay = entry.previous.getTrackComplete() - entry.mixDuration + 0; + *
+ * + * Alternatively, use {@link #setMixDuration(float, float)} to set both the mix duration and recompute the delay:
+ * + *
+	  entry.setMixDuration(0.25f, 0); // mixDuration, delay
+	 * 
+ */ + 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 delay (ie the mix ends at - * (delay = 0) or before (delay < 0) the previous track entry duration). If the previous - * entry is looping, its next loop completion is used instead of its duration. */ + * @param delay If > 0, sets {@link #getDelay()}. If <= 0, the delay set is the duration of the previous track entry minus + * the specified mix duration plus the specified delay (ie the mix ends at (when delay = + * 0) or before (when delay < 0) the previous track entry duration). If the previous entry is + * looping, its next loop completion is used instead of its duration. */ setMixDuration (mixDuration: number, delay?: number) { 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: + *
  • Bit 0, FIRST: 0 = mix from current pose, 1 = mix from setup pose. Timeline is first to set the property. + *
  • Bit 1, HOLD: 0 = mix out using alphaMix, 1 = apply full alpha to prevent dipping. Timeline is first on its track to + * set the property and the next entry (mixingTo) also sets it. When held, timelineHoldMix's mix controls how the hold fades + * out (for 3+ entry chains where the chain eventually stops setting the property). */ timelineMode = [] 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. - *

    + * * 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. - *

    + * * TrackEntry events are collected during {@link AnimationState#update} and {@link AnimationState#apply} and * fired only after those methods are finished. - *

    - * 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.
    - * 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 not have a timeline to set this property.
    - * 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.
    - * 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 not have a timeline to set this property.
    - * 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; diff --git a/spine-ts/spine-core/src/AnimationStateData.ts b/spine-ts/spine-core/src/AnimationStateData.ts index e98df1e35..df9ac3e4c 100644 --- a/spine-ts/spine-core/src/AnimationStateData.ts +++ b/spine-ts/spine-core/src/AnimationStateData.ts @@ -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]; diff --git a/spine-ts/spine-core/src/AtlasAttachmentLoader.ts b/spine-ts/spine-core/src/AtlasAttachmentLoader.ts index 608ec8aa8..8702b0ddb 100644 --- a/spine-ts/spine-core/src/AtlasAttachmentLoader.ts +++ b/spine-ts/spine-core/src/AtlasAttachmentLoader.ts @@ -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) diff --git a/spine-ts/spine-core/src/Bone.ts b/spine-ts/spine-core/src/Bone.ts index e85203fb7..4e329b0df 100644 --- a/spine-ts/spine-core/src/Bone.ts +++ b/spine-ts/spine-core/src/Bone.ts @@ -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 { +/** A node in a skeleton's hierarchy with a transform that affects its children and their attachments. A bone has a number of + * poses: + *

      + *
    • {@link #data}: The setup pose. + *
    • {@link #pose}: The unconstrained local pose. Set by animations and application code. + *
    • {@link #appliedPose}: The local pose to use for rendering. Possibly modified by constraints. + *
    • 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)}. + *
    + */ +export class Bone extends PosedActive { /** The parent bone, or null if this is the root bone. */ parent: Bone | null = null; @@ -49,11 +53,11 @@ export class Bone extends PosedActive { 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); diff --git a/spine-ts/spine-core/src/BoneData.ts b/spine-ts/spine-core/src/BoneData.ts index 1b0697a29..1ceddf049 100644 --- a/spine-ts/spine-core/src/BoneData.ts +++ b/spine-ts/spine-core/src/BoneData.ts @@ -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 { - /** The index of the bone in {@link Skeleton.getBones}. */ +export class BoneData extends PosedData { + /** 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 { * 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 { 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; } } diff --git a/spine-ts/spine-core/src/BonePose.ts b/spine-ts/spine-core/src/BonePose.ts index 7ae3b8594..ff0106c18 100644 --- a/spine-ts/spine-core/src/BonePose.ts +++ b/spine-ts/spine-core/src/BonePose.ts @@ -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)}. + *

    + * 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). + *

    + * 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, 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 [a b][c d] 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 [a b][c d] 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 [a b][c d] 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 [a b][c d] 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. - *

    + /** Computes the world transform using the parent bone's world transform and this applied local pose. Child bones are not + * updated. + * * See World transforms 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. - *

    - * 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); diff --git a/spine-ts/spine-core/src/Constraint.ts b/spine-ts/spine-core/src/Constraint.ts index fd0581bad..2cbb126b9 100644 --- a/spine-ts/spine-core/src/Constraint.ts +++ b/spine-ts/spine-core/src/Constraint.ts @@ -38,7 +38,7 @@ export abstract class Constraint< T extends Constraint, D extends ConstraintData, P extends Pose

    > - extends PosedActive implements Update { + extends PosedActive implements Update { constructor (data: D, pose: P, constrained: P) { super(data, pose, constrained); diff --git a/spine-ts/spine-core/src/BoneLocal.ts b/spine-ts/spine-core/src/DrawOrder.ts similarity index 52% rename from spine-ts/spine-core/src/BoneLocal.ts rename to spine-ts/spine-core/src/DrawOrder.ts index 28b68f153..e2f3350dd 100644 --- a/spine-ts/spine-core/src/BoneLocal.ts +++ b/spine-ts/spine-core/src/DrawOrder.ts @@ -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 { +/** 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); } } diff --git a/spine-ts/spine-core/src/Event.ts b/spine-ts/spine-core/src/Event.ts index c1e9e8183..2099cc7b1 100644 --- a/spine-ts/spine-core/src/Event.ts +++ b/spine-ts/spine-core/src/Event.ts @@ -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) { diff --git a/spine-ts/spine-core/src/EventData.ts b/spine-ts/spine-core/src/EventData.ts index 6188b91ef..fced9400e 100644 --- a/spine-ts/spine-core/src/EventData.ts +++ b/spine-ts/spine-core/src/EventData.ts @@ -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; diff --git a/spine-ts/spine-core/src/IkConstraint.ts b/spine-ts/spine-core/src/IkConstraint.ts index 39bee393d..ec996a8fd 100644 --- a/spine-ts/spine-core/src/IkConstraint.ts +++ b/spine-ts/spine-core/src/IkConstraint.ts @@ -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 { @@ -54,7 +54,7 @@ export class IkConstraint extends Constraint { /** 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 { 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. */ + *

    + * 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. diff --git a/spine-ts/spine-core/src/PathConstraint.ts b/spine-ts/spine-core/src/PathConstraint.ts index 535e90baa..e8633cc01 100644 --- a/spine-ts/spine-core/src/PathConstraint.ts +++ b/spine-ts/spine-core/src/PathConstraint.ts @@ -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 { @@ -69,7 +68,7 @@ export class PathConstraint extends Constraint 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 = this.world; const closed = path.closed; let verticesLength = path.worldVerticesLength, curveCount = verticesLength / 6, prevCurve = PathConstraint.NONE; diff --git a/spine-ts/spine-core/src/Physics.ts b/spine-ts/spine-core/src/Physics.ts index 6175d0e26..748996922 100644 --- a/spine-ts/spine-core/src/Physics.ts +++ b/spine-ts/spine-core/src/Physics.ts @@ -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. */ diff --git a/spine-ts/spine-core/src/PhysicsConstraint.ts b/spine-ts/spine-core/src/PhysicsConstraint.ts index 2e38ba645..34dd1585d 100644 --- a/spine-ts/spine-core/src/PhysicsConstraint.ts +++ b/spine-ts/spine-core/src/PhysicsConstraint.ts @@ -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. *

    * See Physics constraints in the Spine User Guide. */ export class PhysicsConstraint extends Constraint { @@ -68,7 +68,7 @@ export class PhysicsConstraint extends Constraint { + + /** 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) { diff --git a/spine-ts/spine-core/src/Pose.ts b/spine-ts/spine-core/src/Pose.ts index 4b1ae76cf..1805c42c2 100644 --- a/spine-ts/spine-core/src/Pose.ts +++ b/spine-ts/spine-core/src/Pose.ts @@ -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

    { + /** Sets this pose to the specified pose. */ set (pose: P): void; } diff --git a/spine-ts/spine-core/src/Posed.ts b/spine-ts/spine-core/src/Posed.ts index 62501e22a..c35d5465c 100644 --- a/spine-ts/spine-core/src/Posed.ts +++ b/spine-ts/spine-core/src/Posed.ts @@ -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: + *

      + *
    • {@link #data}: The setup pose. + *
    • {@link #pose}: The unconstrained pose. Set by animations and application code. + *
    • {@link #appliedPose}: The pose to use for rendering. Possibly modified by constraints. + *
    + */ export abstract class Posed< D extends PosedData

    , - P extends Pose

    , - A extends P> { + P extends Pose

    > { /** 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); } } diff --git a/spine-ts/spine-core/src/PosedActive.ts b/spine-ts/spine-core/src/PosedActive.ts index bf0786992..62bb4b382 100644 --- a/spine-ts/spine-core/src/PosedActive.ts +++ b/spine-ts/spine-core/src/PosedActive.ts @@ -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 extends Pose

    , - A extends P> - extends Posed { + P extends Pose

    > + extends Posed { 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; } diff --git a/spine-ts/spine-core/src/PosedData.ts b/spine-ts/spine-core/src/PosedData.ts index 1533c212a..baad05b9a 100644 --- a/spine-ts/spine-core/src/PosedData.ts +++ b/spine-ts/spine-core/src/PosedData.ts @@ -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

    > { - /** 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}. */ + *

    + * 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; } } diff --git a/spine-ts/spine-core/src/Skeleton.ts b/spine-ts/spine-core/src/Skeleton.ts index 17515fd34..627da39a9 100644 --- a/spine-ts/spine-core/src/Skeleton.ts +++ b/spine-ts/spine-core/src/Skeleton.ts @@ -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; - /** The skeleton's slots. */ + /** The skeleton's slots. To add a slot, also add it to {@link DrawOrder#pose}. */ readonly slots: Array; - /** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */ - drawOrder: Array; + /** 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> = []; + readonly resetCache: Array> = []; /** 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) { - if (object.pose === object.applied) { + constrained (object: Posed) { + 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. *

    * 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. *

    * 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 Runtime skins 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> (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 = new Array(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; diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts index f621dd536..94bc7eaa8 100644 --- a/spine-ts/spine-core/src/SkeletonBinary.ts +++ b/spine-ts/spine-core/src/SkeletonBinary.ts @@ -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(); diff --git a/spine-ts/spine-core/src/SkeletonBounds.ts b/spine-ts/spine-core/src/SkeletonBounds.ts index d01c085c4..9f767bf7d 100644 --- a/spine-ts/spine-core/src/SkeletonBounds.ts +++ b/spine-ts/spine-core/src/SkeletonBounds.ts @@ -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); diff --git a/spine-ts/spine-core/src/SkeletonData.ts b/spine-ts/spine-core/src/SkeletonData.ts index 0aff922f0..db3482b7d 100644 --- a/spine-ts/spine-core/src/SkeletonData.ts +++ b/spine-ts/spine-core/src/SkeletonData.ts @@ -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> (constraintName: string, type: new (name: string) => T): T | null { if (!constraintName) throw new Error("constraintName cannot be null."); diff --git a/spine-ts/spine-core/src/SkeletonJson.ts b/spine-ts/spine-core/src/SkeletonJson.ts index c293bc6e8..b36a0a09c 100644 --- a/spine-ts/spine-core/src/SkeletonJson.ts +++ b/spine-ts/spine-core/src/SkeletonJson.ts @@ -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); } diff --git a/spine-ts/spine-core/src/SkeletonRendererCore.ts b/spine-ts/spine-core/src/SkeletonRendererCore.ts index 413eb5128..3a6a3f51d 100644 --- a/spine-ts/spine-core/src/SkeletonRendererCore.ts +++ b/spine-ts/spine-core/src/SkeletonRendererCore.ts @@ -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)) { diff --git a/spine-ts/spine-core/src/Skin.ts b/spine-ts/spine-core/src/Skin.ts index b4b6e84d3..54bb2668e 100644 --- a/spine-ts/spine-core/src/Skin.ts +++ b/spine-ts/spine-core/src/Skin.ts @@ -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. + *

    + * See {@link SkeletonData#findSkin(String)}. */ name: string; attachments = [] as StringMap[]; @@ -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; } diff --git a/spine-ts/spine-core/src/Slider.ts b/spine-ts/spine-core/src/Slider.ts index 341390c50..a8318c433 100644 --- a/spine-ts/spine-core/src/Slider.ts +++ b/spine-ts/spine-core/src/Slider.ts @@ -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 Physics constraints in the Spine User Guide. */ +/** Applies an animation based on either the slider's {@link SliderPose#time} or a bone's transform property. + *

    + * See Sliders in the Spine User Guide. */ export class Slider extends Constraint { 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 { } 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 { 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 { 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++) diff --git a/spine-ts/spine-core/src/SliderData.ts b/spine-ts/spine-core/src/SliderData.ts index 0e6e490e8..e19d61eb1 100644 --- a/spine-ts/spine-core/src/SliderData.ts +++ b/spine-ts/spine-core/src/SliderData.ts @@ -39,13 +39,29 @@ import type { FromProperty } from "./TransformConstraintData.js"; * * See Slider constraints in the Spine User Guide. */ export class SliderData extends ConstraintData { + + /** 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) { diff --git a/spine-ts/spine-core/src/SliderPose.ts b/spine-ts/spine-core/src/SliderPose.ts index 5df4cf5eb..f50ba4af3 100644 --- a/spine-ts/spine-core/src/SliderPose.ts +++ b/spine-ts/spine-core/src/SliderPose.ts @@ -31,7 +31,11 @@ import type { Pose } from "./Pose.js"; /** Stores a pose for a slider. */ export class SliderPose implements Pose { + + /** 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) { diff --git a/spine-ts/spine-core/src/Slot.ts b/spine-ts/spine-core/src/Slot.ts index 8c61df540..b5bf4c1be 100644 --- a/spine-ts/spine-core/src/Slot.ts +++ b/spine-ts/spine-core/src/Slot.ts @@ -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 { +/** Organizes 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 { readonly skeleton: Skeleton; /** The bone this slot belongs to. */ @@ -50,18 +51,29 @@ export class Slot extends Posed { 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 { diff --git a/spine-ts/spine-core/src/SlotData.ts b/spine-ts/spine-core/src/SlotData.ts index 62d4ba1b1..7d9c20797 100644 --- a/spine-ts/spine-core/src/SlotData.ts +++ b/spine-ts/spine-core/src/SlotData.ts @@ -34,7 +34,7 @@ import { SlotPose } from "./SlotPose.js"; /** Stores the setup pose for a {@link Slot}. */ export class SlotData extends PosedData { - /** 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. */ diff --git a/spine-ts/spine-core/src/SlotPose.ts b/spine-ts/spine-core/src/SlotPose.ts index e7f73372d..9b15c4d6d 100644 --- a/spine-ts/spine-core/src/SlotPose.ts +++ b/spine-ts/spine-core/src/SlotPose.ts @@ -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 { - /** 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 diff --git a/spine-ts/spine-core/src/TransformConstraint.ts b/spine-ts/spine-core/src/TransformConstraint.ts index 04dc064c9..008f5ca70 100644 --- a/spine-ts/spine-core/src/TransformConstraint.ts +++ b/spine-ts/spine-core/src/TransformConstraint.ts @@ -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 { @@ -55,7 +54,7 @@ export class TransformConstraint extends Constraint { - /** 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) { diff --git a/spine-ts/spine-core/src/Utils.ts b/spine-ts/spine-core/src/Utils.ts index 7a6a00aa3..f0bffc38c 100644 --- a/spine-ts/spine-core/src/Utils.ts +++ b/spine-ts/spine-core/src/Utils.ts @@ -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 { @@ -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 (array: Array, 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}`); } } diff --git a/spine-ts/spine-core/src/attachments/Attachment.ts b/spine-ts/spine-core/src/attachments/Attachment.ts index dfba6a725..c743f050d 100644 --- a/spine-ts/spine-core/src/attachments/Attachment.ts +++ b/spine-ts/spine-core/src/attachments/Attachment.ts @@ -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 | 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. + *

    + * See World transforms 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} - 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. */ 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; diff --git a/spine-ts/spine-core/src/attachments/ClippingAttachment.ts b/spine-ts/spine-core/src/attachments/ClippingAttachment.ts index d37b66166..c500a46fe 100644 --- a/spine-ts/spine-core/src/attachments/ClippingAttachment.ts +++ b/spine-ts/spine-core/src/attachments/ClippingAttachment.ts @@ -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; diff --git a/spine-ts/spine-core/src/attachments/HasSequence.ts b/spine-ts/spine-core/src/attachments/HasSequence.ts index 50709f313..42747e294 100644 --- a/spine-ts/spine-core/src/attachments/HasSequence.ts +++ b/spine-ts/spine-core/src/attachments/HasSequence.ts @@ -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; } diff --git a/spine-ts/spine-core/src/attachments/MeshAttachment.ts b/spine-ts/spine-core/src/attachments/MeshAttachment.ts index 56e9eddfc..d832c66f5 100644 --- a/spine-ts/spine-core/src/attachments/MeshAttachment.ts +++ b/spine-ts/spine-core/src/attachments/MeshAttachment.ts @@ -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 = []; /** 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; } diff --git a/spine-ts/spine-core/src/attachments/PathAttachment.ts b/spine-ts/spine-core/src/attachments/PathAttachment.ts index ee97a4986..a4491b76f 100644 --- a/spine-ts/spine-core/src/attachments/PathAttachment.ts +++ b/spine-ts/spine-core/src/attachments/PathAttachment.ts @@ -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 diff --git a/spine-ts/spine-core/src/attachments/PointAttachment.ts b/spine-ts/spine-core/src/attachments/PointAttachment.ts index ca6d62940..70ed0d973 100644 --- a/spine-ts/spine-core/src/attachments/PointAttachment.ts +++ b/spine-ts/spine-core/src/attachments/PointAttachment.ts @@ -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; diff --git a/spine-ts/spine-core/src/attachments/RegionAttachment.ts b/spine-ts/spine-core/src/attachments/RegionAttachment.ts index 326927970..e9d6ee76f 100644 --- a/spine-ts/spine-core/src/attachments/RegionAttachment.ts +++ b/spine-ts/spine-core/src/attachments/RegionAttachment.ts @@ -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 worldVertices 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; diff --git a/spine-ts/spine-core/src/attachments/Sequence.ts b/spine-ts/spine-core/src/attachments/Sequence.ts index 81fd4c49f..add30182a 100644 --- a/spine-ts/spine-core/src/attachments/Sequence.ts +++ b/spine-ts/spine-core/src/attachments/Sequence.ts @@ -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; + 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 count should be 1. */ constructor (count: number, pathSuffix: boolean) { this.regions = new Array(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, diff --git a/spine-ts/spine-core/src/index.ts b/spine-ts/spine-core/src/index.ts index 155f8a0bb..f3e901e6f 100644 --- a/spine-ts/spine-core/src/index.ts +++ b/spine-ts/spine-core/src/index.ts @@ -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'; diff --git a/spine-ts/spine-phaser-v3/src/SpineGameObject.ts b/spine-ts/spine-phaser-v3/src/SpineGameObject.ts index 99df76868..4ec5c1a31 100644 --- a/spine-ts/spine-phaser-v3/src/SpineGameObject.ts +++ b/spine-ts/spine-phaser-v3/src/SpineGameObject.ts @@ -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(); diff --git a/spine-ts/spine-phaser-v4/src/SpineGameObject.ts b/spine-ts/spine-phaser-v4/src/SpineGameObject.ts index b46d5046b..2069a4b7e 100644 --- a/spine-ts/spine-phaser-v4/src/SpineGameObject.ts +++ b/spine-ts/spine-phaser-v4/src/SpineGameObject.ts @@ -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(); diff --git a/spine-ts/spine-pixi-v7/src/Spine.ts b/spine-ts/spine-pixi-v7/src/Spine.ts index f624abed0..44ce24079 100644 --- a/spine-ts/spine-pixi-v7/src/Spine.ts +++ b/spine-ts/spine-pixi-v7/src/Spine.ts @@ -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 | 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); } } diff --git a/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts b/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts index c31ce81f9..6ffb64f57 100644 --- a/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts +++ b/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts @@ -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; diff --git a/spine-ts/spine-pixi-v8/src/Spine.ts b/spine-ts/spine-pixi-v8/src/Spine.ts index 88db1a839..9b6271c19 100644 --- a/spine-ts/spine-pixi-v8/src/Spine.ts +++ b/spine-ts/spine-pixi-v8/src/Spine.ts @@ -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); } } diff --git a/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts b/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts index 7f00b85ee..2e1786383 100644 --- a/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts +++ b/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts @@ -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; diff --git a/spine-ts/spine-pixi-v8/src/SpinePipe.ts b/spine-ts/spine-pixi-v8/src/SpinePipe.ts index fd4fba0a7..c19e9db39 100644 --- a/spine-ts/spine-pixi-v8/src/SpinePipe.ts +++ b/spine-ts/spine-pixi-v8/src/SpinePipe.ts @@ -87,14 +87,14 @@ export class SpinePipe implements RenderPipe { // 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 { 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 { 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.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); diff --git a/spine-ts/spine-player/src/Player.ts b/spine-ts/spine-player/src/Player.ts index 0eb05759c..03f852f00 100644 --- a/spine-ts/spine-player/src/Player.ts +++ b/spine-ts/spine-player/src/Player.ts @@ -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); } diff --git a/spine-ts/spine-threejs/src/SkeletonMesh.ts b/spine-ts/spine-threejs/src/SkeletonMesh.ts index 4ba6b4d6b..0004f539f 100644 --- a/spine-ts/spine-threejs/src/SkeletonMesh.ts +++ b/spine-ts/spine-threejs/src/SkeletonMesh.ts @@ -225,7 +225,7 @@ export class SkeletonMesh extends THREE.Object3D { let triangles: Array | 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; diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts index 055766e72..5311e84ac 100644 --- a/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts +++ b/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts @@ -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) { diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts index 1d9c4800d..ff4a7a4bc 100644 --- a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts +++ b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts @@ -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()); diff --git a/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts b/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts index 8149fd576..553897d1a 100644 --- a/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts +++ b/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts @@ -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); diff --git a/spine-ts/spine-webgl/src/SkeletonRenderer.ts b/spine-ts/spine-webgl/src/SkeletonRenderer.ts index 0cadc213e..2c448db93 100644 --- a/spine-ts/spine-webgl/src/SkeletonRenderer.ts +++ b/spine-ts/spine-webgl/src/SkeletonRenderer.ts @@ -69,7 +69,7 @@ export class SkeletonRenderer { const renderable: Renderable = this.renderable; let uvs: NumberArrayLike; let triangles: Array; - 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) {