diff --git a/spine-ts/spine-canvas/example/index.html b/spine-ts/spine-canvas/example/index.html index 5316b2554..f52a419ec 100644 --- a/spine-ts/spine-canvas/example/index.html +++ b/spine-ts/spine-canvas/example/index.html @@ -37,7 +37,7 @@ // Instantiate a new skeleton based on the atlas and skeleton data. skeleton = new spine.Skeleton(skeletonData); - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(spine.Physics.update); bounds = skeleton.getBoundsRect(); diff --git a/spine-ts/spine-canvas/example/mouse-click.html b/spine-ts/spine-canvas/example/mouse-click.html index f43414c1c..aecc5ca6a 100644 --- a/spine-ts/spine-canvas/example/mouse-click.html +++ b/spine-ts/spine-canvas/example/mouse-click.html @@ -37,7 +37,7 @@ // Instantiate a new skeleton based on the atlas and skeleton data. skeleton = new spine.Skeleton(skeletonData); - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(spine.Physics.update); bounds = skeleton.getBoundsRect(); diff --git a/spine-ts/spine-canvas/src/SkeletonRenderer.ts b/spine-ts/spine-canvas/src/SkeletonRenderer.ts index 398e76d33..076a446f3 100644 --- a/spine-ts/spine-canvas/src/SkeletonRenderer.ts +++ b/spine-ts/spine-canvas/src/SkeletonRenderer.ts @@ -61,18 +61,19 @@ export class SkeletonRenderer { if (this.debugRendering) ctx.strokeStyle = "green"; for (let i = 0, n = drawOrder.length; i < n; i++) { - let slot = drawOrder[i]; + const slot = drawOrder[i]; let bone = slot.bone; if (!bone.active) continue; - let attachment = slot.getAttachment(); + let pose = slot.pose; + let attachment = pose.attachment; if (!(attachment instanceof RegionAttachment)) continue; attachment.computeWorldVertices(slot, worldVertices, 0, 2); let region: TextureRegion = attachment.region; let image: HTMLImageElement = (region.texture).getImage() as HTMLImageElement; - let slotColor = slot.color; + let slotColor = pose.color; let regionColor = attachment.color; color.set(skeletonColor.r * slotColor.r * regionColor.r, skeletonColor.g * slotColor.g * regionColor.g, @@ -80,7 +81,8 @@ export class SkeletonRenderer { skeletonColor.a * slotColor.a * regionColor.a); ctx.save(); - ctx.transform(bone.a, bone.c, bone.b, bone.d, bone.worldX, bone.worldY); + const boneApplied = bone.applied; + ctx.transform(boneApplied.a, boneApplied.c, boneApplied.b, boneApplied.d, boneApplied.worldX, boneApplied.worldY); ctx.translate(attachment.offset[0], attachment.offset[1]); ctx.rotate(attachment.rotation * Math.PI / 180); @@ -116,8 +118,9 @@ export class SkeletonRenderer { let triangles: Array | null = null; for (let i = 0, n = drawOrder.length; i < n; i++) { - let slot = drawOrder[i]; - let attachment = slot.getAttachment(); + const slot = drawOrder[i]; + let pose = slot.pose; + let attachment = pose.attachment; let texture: HTMLImageElement; let region: TextureAtlasRegion; @@ -137,7 +140,7 @@ export class SkeletonRenderer { if (texture) { if (slot.data.blendMode != blendMode) blendMode = slot.data.blendMode; - let slotColor = slot.color; + let slotColor = pose.color; let attachmentColor = attachment.color; color.set(skeletonColor.r * slotColor.r * attachmentColor.r, skeletonColor.g * slotColor.g * attachmentColor.g, @@ -225,8 +228,8 @@ export class SkeletonRenderer { } private computeRegionVertices (slot: Slot, region: RegionAttachment, pma: boolean) { - let skeletonColor = slot.bone.skeleton.color; - let slotColor = slot.color; + let skeletonColor = slot.skeleton.color; + let slotColor = slot.pose.color; let regionColor = region.color; let alpha = skeletonColor.a * slotColor.a * regionColor.a; let multiplier = pma ? alpha : 1; @@ -273,8 +276,9 @@ export class SkeletonRenderer { } private computeMeshVertices (slot: Slot, mesh: MeshAttachment, pma: boolean) { - let skeletonColor = slot.bone.skeleton.color; - let slotColor = slot.color; + let skeleton = slot.skeleton; + let skeletonColor = skeleton.color; + let slotColor = slot.pose.color; let regionColor = mesh.color; let alpha = skeletonColor.a * slotColor.a * regionColor.a; let multiplier = pma ? alpha : 1; @@ -287,7 +291,7 @@ export class SkeletonRenderer { let vertexCount = mesh.worldVerticesLength / 2; let vertices = this.vertices; if (vertices.length < mesh.worldVerticesLength) this.vertices = vertices = Utils.newFloatArray(mesh.worldVerticesLength); - mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE); + mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE); let uvs = mesh.uvs; for (let i = 0, u = 0, v = 2; i < vertexCount; i++) { diff --git a/spine-ts/spine-canvaskit/src/index.ts b/spine-ts/spine-canvaskit/src/index.ts index 17b522964..abe581a1d 100644 --- a/spine-ts/spine-canvaskit/src/index.ts +++ b/spine-ts/spine-canvaskit/src/index.ts @@ -24,7 +24,6 @@ import { } from "@esotericsoftware/spine-core"; import { Canvas, - Surface, CanvasKit, Image, Paint, @@ -232,11 +231,12 @@ export class SkeletonRenderer { for (let i = 0, n = drawOrder.length; i < n; i++) { let slot = drawOrder[i]; if (!slot.bone.active) { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } - let attachment = slot.getAttachment(); + let pose = slot.pose; + let attachment = pose.attachment; let positions = this.scratchPositions; let colors = this.scratchColors; let uvs: NumberArrayLike; @@ -261,6 +261,7 @@ export class SkeletonRenderer { : positions; numVertices = mesh.worldVerticesLength >> 1; mesh.computeWorldVertices( + skeleton, slot, 0, mesh.worldVerticesLength, @@ -274,10 +275,10 @@ export class SkeletonRenderer { attachmentColor = mesh.color; } else if (attachment instanceof ClippingAttachment) { let clip = attachment as ClippingAttachment; - clipper.clipStart(slot, clip); + clipper.clipStart(skeleton, slot, clip); continue; } else { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } @@ -294,7 +295,7 @@ export class SkeletonRenderer { triangles = clipper.clippedTriangles; } - let slotColor = slot.color; + let slotColor = pose.color; let finalColor = this.tempColor; finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; @@ -338,7 +339,7 @@ export class SkeletonRenderer { vertices.delete(); } - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); } clipper.clipEnd(); } diff --git a/spine-ts/spine-core/src/Animation.ts b/spine-ts/spine-core/src/Animation.ts index 23f9f23ae..f10ebe926 100644 --- a/spine-ts/spine-core/src/Animation.ts +++ b/spine-ts/spine-core/src/Animation.ts @@ -28,11 +28,9 @@ *****************************************************************************/ import { VertexAttachment, Attachment } from "./attachments/Attachment.js"; -import { IkConstraint } from "./IkConstraint.js"; import { PathConstraint } from "./PathConstraint.js"; import { Skeleton } from "./Skeleton.js"; import { Slot } from "./Slot.js"; -import { TransformConstraint } from "./TransformConstraint.js"; import { StringSet, Utils, MathUtils, NumberArrayLike } from "./Utils.js"; import { Event } from "./Event.js"; import { HasTextureRegion } from "./attachments/HasTextureRegion.js"; @@ -40,30 +38,49 @@ import { SequenceMode, SequenceModeValues } from "./attachments/Sequence.js"; import { PhysicsConstraint } from "./PhysicsConstraint.js"; import { PhysicsConstraintData } from "./PhysicsConstraintData.js"; import { Inherit } from "./BoneData.js"; +import { BoneLocal } from "./BoneLocal.js"; +import { SlotPose } from "./SlotPose.js"; +import { PhysicsConstraintPose } from "./PhysicsConstraintPose.js"; /** A simple container for a list of timelines and a name. */ export class Animation { /** The animation's name, which is unique across all animations in the skeleton. */ - name: string; - timelines: Array = []; - timelineIds: StringSet = new StringSet(); + readonly name: string; - /** The duration of the animation in seconds, which is the highest time of all keys in the timeline. */ + /** If the returned array or the timelines it contains are modified, {@link setTimelines()} must be called. */ + timelines: Array = []; + + readonly timelineIds: StringSet; + 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 + * used to know when it has completed and when it should loop back to the start. */ duration: number; constructor (name: string, timelines: Array, duration: number) { if (!name) throw new Error("name cannot be null."); this.name = name; - this.setTimelines(timelines); this.duration = duration; + this.timelineIds = new StringSet(); + this.bones = new Array(); + this.setTimelines(timelines); } setTimelines (timelines: Array) { if (!timelines) throw new Error("timelines cannot be null."); this.timelines = timelines; + + const n = timelines.length; this.timelineIds.clear(); - for (var i = 0; i < timelines.length; i++) - this.timelineIds.addAll(timelines[i].getPropertyIds()); + this.bones.length = 0; + const boneSet = new Set(); + const items = timelines; + for (let i = 0; i < n; i++) { + const timeline = items[i]; + this.timelineIds.addAll(timeline.getPropertyIds()); + if (isBoneTimeline(timeline) && boneSet.add(timeline.boneIndex)) + this.bones.push(timeline.boneIndex); + } } hasTimeline (ids: string[]): boolean { @@ -72,12 +89,29 @@ export class Animation { return false; } - /** Applies all the animation's timelines to the specified skeleton. + /** Applies the animation's timelines to the specified skeleton. * - * See Timeline {@link Timeline#apply(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. - * @param loop If true, the animation repeats after {@link #getDuration()}. - * @param events May be null to ignore fired events. */ - apply (skeleton: Skeleton, lastTime: number, time: number, loop: boolean, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { + * 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 + * 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. */ + apply (skeleton: Skeleton, lastTime: number, time: number, loop: boolean, events: Array | null, alpha: number, + blend: MixBlend, direction: MixDirection, appliedPose: boolean) { if (!skeleton) throw new Error("skeleton cannot be null."); if (loop && this.duration != 0) { @@ -87,83 +121,79 @@ export class Animation { let timelines = this.timelines; for (let i = 0, n = timelines.length; i < n; i++) - timelines[i].apply(skeleton, lastTime, time, events, alpha, blend, direction); + timelines[i].apply(skeleton, lastTime, time, events, alpha, blend, direction, 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(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */ + * See Timeline {@link Timeline.apply}. */ export enum MixBlend { - /** Transitions from the setup value to the timeline value (the current value is not used). Before the first key, the setup - * value is set. */ + /** 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 from the current value to the timeline value. Before the first key, transitions from the current value to - * the setup value. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or - * {@link AttachmentTimeline}, use the setup value before the first key. - * - * `first` is intended for the first animations applied, not for animations layered on top of those. */ + /** 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 from the current value to the timeline value. No change is made before the first key (the current value is - * kept until the first key). - * - * `replace` is intended for animations layered on top of others, not for the first animations applied. */ + /** 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 from the current value to the current value plus the timeline value. No change is made before the first key - * (the current value is kept until the first key). - * - * `add` is intended for animations layered on top of others, not for the first animations applied. Properties - * keyed by additive animations must be set manually or by another animation before applying the additive animations, else - * the property values will increase continually. */ + /** 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(Skeleton, float, float, Array, float, MixBlend, MixDirection)}. */ + * See Timeline {@link Timeline#apply}. */ export enum MixDirection { - mixIn, mixOut + in, out } -const Property = { - rotate: 0, - x: 1, - y: 2, - scaleX: 3, - scaleY: 4, - shearX: 5, - shearY: 6, - inherit: 7, - - rgb: 8, - alpha: 9, - rgb2: 10, - - attachment: 11, - deform: 12, - - event: 13, - drawOrder: 14, - - ikConstraint: 15, - transformConstraint: 16, - - pathConstraintPosition: 17, - pathConstraintSpacing: 18, - pathConstraintMix: 19, - - physicsConstraintInertia: 20, - physicsConstraintStrength: 21, - physicsConstraintDamping: 22, - physicsConstraintMass: 23, - physicsConstraintWind: 24, - physicsConstraintGravity: 25, - physicsConstraintMix: 26, - physicsConstraintReset: 27, - - sequence: 28, +export enum Property { + rotate, + x, + y, + scaleX, + scaleY, + shearX, + shearY, + inherit, + rgb, + alpha, + rgb2, + attachment, + deform, + event, + drawOrder, + ikConstraint, + transformConstraint, + pathConstraintPosition, + pathConstraintSpacing, + pathConstraintMix, + physicsConstraintInertia, + physicsConstraintStrength, + physicsConstraintDamping, + physicsConstraintMass, + physicsConstraintWind, + physicsConstraintGravity, + physicsConstraintMix, + physicsConstraintReset, + sequence, + sliderTime, + sliderMix, } /** The interface for all timelines. */ @@ -171,7 +201,7 @@ export abstract class Timeline { propertyIds: string[]; frames: NumberArrayLike; - constructor (frameCount: number, propertyIds: string[]) { + constructor (frameCount: number, ...propertyIds: string[]) { this.propertyIds = propertyIds; this.frames = Utils.newFloatArray(frameCount * this.getFrameEntries()); } @@ -192,16 +222,54 @@ export abstract class Timeline { return this.frames[this.frames.length - this.getFrameEntries()]; } - abstract apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, blend: MixBlend, direction: MixDirection): void; + /** 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. */ + abstract apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, + blend: MixBlend, direction: MixDirection, appliedPose: boolean): void; - static search1 (frames: NumberArrayLike, time: number) { + /** Linear search using a stride of 1. + * @param time Must be >= the first value in frames. + * @return The index of the first value <= time. */ + static search (frames: NumberArrayLike, time: number): number; + + /** Linear search using the specified stride. + * @param time Must be >= the first value in frames. + * @return The index of the first value <= time. */ + static search (frames: NumberArrayLike, time: number, step: number): number; + + static search (frames: NumberArrayLike, time: number, step?: number): number { + if (step === undefined) + return Timeline.search1(frames, time); + else + return Timeline.search2(frames, time, step); + } + + private static search1 (frames: NumberArrayLike, time: number) { let n = frames.length; for (let i = 1; i < n; i++) if (frames[i] > time) return i - 1; return n - 1; } - static search (frames: NumberArrayLike, time: number, step: number) { + private static search2 (frames: NumberArrayLike, time: number, step: number) { let n = frames.length; for (let i = step; i < n; i += step) if (frames[i] > time) return i - step; @@ -209,22 +277,22 @@ export abstract class Timeline { } } -export interface BoneTimeline { - /** The index of the bone in {@link Skeleton#bones} that will be changed. */ - boneIndex: number; -} - +/** An interface for timelines which change the property of a slot. */ export interface SlotTimeline { - /** The index of the slot in {@link Skeleton#slots} that will be changed. */ + /** The index of the slot in {@link Skeleton.slots} that will be changed when this timeline is applied. */ slotIndex: number; } -/** The base class for timelines that use interpolation between key frame values. */ +export function isSlotTimeline(obj: any): obj is SlotTimeline { + return typeof obj === 'object' && obj !== null && typeof obj.slotIndex === 'number'; +} + +/** The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. */ export abstract class CurveTimeline extends Timeline { protected curves: NumberArrayLike; // type, x, y, ... - constructor (frameCount: number, bezierCount: number, propertyIds: string[]) { - super(frameCount, propertyIds); + constructor (frameCount: number, bezierCount: number, ...propertyIds: string[]) { + super(frameCount, ...propertyIds); this.curves = Utils.newFloatArray(frameCount + bezierCount * 18/*BEZIER_SIZE*/); this.curves[frameCount - 1] = 1/*STEPPED*/; } @@ -311,7 +379,7 @@ export abstract class CurveTimeline extends Timeline { export abstract class CurveTimeline1 extends CurveTimeline { constructor (frameCount: number, bezierCount: number, propertyId: string) { - super(frameCount, bezierCount, [propertyId]); + super(frameCount, bezierCount, propertyId); } getFrameEntries () { @@ -361,62 +429,66 @@ export abstract class CurveTimeline1 extends CurveTimeline { } let value = this.getCurveValue(time); switch (blend) { - case MixBlend.setup: - return setup + value * alpha; + case MixBlend.setup: return setup + value * alpha; case MixBlend.first: - case MixBlend.replace: - value += setup - current; + case MixBlend.replace: return current + (value + setup - current) * alpha; + case MixBlend.add: return current + value * alpha;; } - return current + value * alpha; } - getAbsoluteValue (time: number, alpha: number, blend: MixBlend, current: number, setup: number) { + getAbsoluteValue(time: number, alpha: number, blend: MixBlend, current: number, setup: number, value?: number) { + if (value === undefined) + return this.getAbsoluteValue1(time, alpha, blend, current, setup); + else + return this.getAbsoluteValue2(time, alpha, blend, 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; + case MixBlend.setup: return setup; + case MixBlend.first: return current + (setup - current) * alpha; + default: return current; } - return current; } let value = this.getCurveValue(time); - if (blend == MixBlend.setup) return setup + (value - setup) * alpha; - return current + (value - current) * alpha; + 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; + } } - getAbsoluteValue2 (time: number, alpha: number, blend: MixBlend, current: number, setup: number, value: number) { + 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; + case MixBlend.setup: return setup; + case MixBlend.first: return current + (setup - current) * alpha; + default: return current; } - return current; } - if (blend == MixBlend.setup) return setup + (value - setup) * alpha; - return current + (value - current) * alpha; + 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; + } } 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; + case MixBlend.setup: return setup; + case MixBlend.first: return current + (setup - current) * alpha; + default: return current; } - return current; } let value = this.getCurveValue(time) * setup; - if (alpha == 1) { - if (blend == MixBlend.add) return current + value - setup; - return value; - } + 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.mixOut) { + if (direction == MixDirection.out) { switch (blend) { case MixBlend.setup: return setup + (Math.abs(value) * MathUtils.signum(setup) - setup) * alpha; @@ -440,12 +512,15 @@ export abstract class CurveTimeline1 extends CurveTimeline { } } -/** The base class for a {@link CurveTimeline} which sets two properties. */ -export abstract class CurveTimeline2 extends CurveTimeline { +/** The base class for a {@link CurveTimeline} that is a {@link BoneTimeline} and sets two properties. */ +export abstract class BoneTimeline2 extends CurveTimeline implements BoneTimeline { + readonly boneIndex; + /** @param bezierCount The maximum number of Bezier curves. See {@link #shrink(int)}. * @param propertyIds Unique identifiers for the properties the timeline modifies. */ - constructor (frameCount: number, bezierCount: number, propertyId1: string, propertyId2: string) { - super(frameCount, bezierCount, [propertyId1, propertyId2]); + constructor (frameCount: number, bezierCount: number, boneIndex: number, property1: Property, property2: Property) { + super(frameCount, bezierCount, property1 + "|" + boneIndex, property2 + "|" + boneIndex); + this.boneIndex = boneIndex; } getFrameEntries () { @@ -461,49 +536,73 @@ export abstract class CurveTimeline2 extends CurveTimeline { this.frames[frame + 1/*VALUE1*/] = value1; this.frames[frame + 2/*VALUE2*/] = value2; } + + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, + blend: MixBlend, direction: MixDirection, appliedPose: boolean): void { + let 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; +} + +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: any): obj is 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}. */ -export class RotateTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +export class RotateTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.rotate + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.rotate); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array | null, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.rotation = this.getRelativeValue(time, alpha, blend, bone.rotation, bone.data.rotation); + apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha:number, blend: MixBlend, direction: MixDirection) { + pose.rotation = this.getRelativeValue(time, alpha, blend, pose.rotation, setup.rotation); } } -/** Changes a bone's local {@link Bone#x} and {@link Bone#y}. */ -export class TranslateTimeline extends CurveTimeline2 implements BoneTimeline { - boneIndex = 0; - +/** Changes a bone's local {@link BoneLocal.x} and {@link BoneLocal.y}. */ +export class TranslateTimeline extends BoneTimeline2 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, - Property.x + "|" + boneIndex, - Property.y + "|" + boneIndex, - ); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.x, Property.y); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (!bone.active) return; - + apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha:number, blend: MixBlend,direction: MixDirection) { let frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.setup: - bone.x = bone.data.x; - bone.y = bone.data.y; + pose.x = setup.x; + pose.y = setup.y; return; case MixBlend.first: - bone.x += (bone.data.x - bone.x) * alpha; - bone.y += (bone.data.y - bone.y) * alpha; + pose.x += (setup.x - pose.x) * alpha; + pose.y += (setup.y - pose.y) * alpha; } return; } @@ -531,77 +630,62 @@ export class TranslateTimeline extends CurveTimeline2 implements BoneTimeline { switch (blend) { case MixBlend.setup: - bone.x = bone.data.x + x * alpha; - bone.y = bone.data.y + y * alpha; + pose.x = setup.x + x * alpha; + pose.y = setup.y + y * alpha; break; case MixBlend.first: case MixBlend.replace: - bone.x += (bone.data.x + x - bone.x) * alpha; - bone.y += (bone.data.y + y - bone.y) * alpha; + pose.x += (setup.x + x - pose.x) * alpha; + pose.y += (setup.y + y - pose.y) * alpha; break; case MixBlend.add: - bone.x += x * alpha; - bone.y += y * alpha; + pose.x += x * alpha; + pose.y += y * alpha; } } } -/** Changes a bone's local {@link Bone#x}. */ -export class TranslateXTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +/** Changes a bone's local {@link BoneLocal.x}. */ +export class TranslateXTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.x + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.x); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.x = this.getRelativeValue(time, alpha, blend, bone.x, bone.data.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); } } -/** Changes a bone's local {@link Bone#x}. */ -export class TranslateYTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +/** Changes a bone's local {@link BoneLocal.y}. */ +export class TranslateYTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.y + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.y); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.y = this.getRelativeValue(time, alpha, blend, bone.y, bone.data.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); } } -/** Changes a bone's local {@link Bone#scaleX)} and {@link Bone#scaleY}. */ -export class ScaleTimeline extends CurveTimeline2 implements BoneTimeline { +/** Changes a bone's local {@link BoneLocal.scaleX} and {@link BoneLocal.scaleY}. */ +export class ScaleTimeline extends BoneTimeline2 { boneIndex = 0; constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, - Property.scaleX + "|" + boneIndex, - Property.scaleY + "|" + boneIndex - ); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.scaleX, Property.scaleY); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (!bone.active) return; - + protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha:number, blend: MixBlend,direction: MixDirection) { let frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.setup: - bone.scaleX = bone.data.scaleX; - bone.scaleY = bone.data.scaleY; + pose.scaleX = setup.scaleX; + pose.scaleY = setup.scaleY; return; case MixBlend.first: - bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; - bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; + pose.scaleX += (setup.scaleX - pose.scaleX) * alpha; + pose.scaleY += (setup.scaleY - pose.scaleY) * alpha; } return; } @@ -626,118 +710,101 @@ export class ScaleTimeline extends CurveTimeline2 implements BoneTimeline { x = this.getBezierValue(time, i, 1/*VALUE1*/, curveType - 2/*BEZIER*/); y = this.getBezierValue(time, i, 2/*VALUE2*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); } - x *= bone.data.scaleX; - y *= bone.data.scaleY; + x *= setup.scaleX; + y *= setup.scaleY; if (alpha == 1) { if (blend == MixBlend.add) { - bone.scaleX += x - bone.data.scaleX; - bone.scaleY += y - bone.data.scaleY; + pose.scaleX += x - setup.scaleX; + pose.scaleY += y - setup.scaleY; } else { - bone.scaleX = x; - bone.scaleY = y; + pose.scaleX = x; + pose.scaleY = y; } } else { let bx = 0, by = 0; - if (direction == MixDirection.mixOut) { + if (direction == MixDirection.out) { switch (blend) { case MixBlend.setup: - bx = bone.data.scaleX; - by = bone.data.scaleY; - bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; - bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; + 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 = bone.scaleX; - by = bone.scaleY; - bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha; - bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha; + 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: - bone.scaleX += (x - bone.data.scaleX) * alpha; - bone.scaleY += (y - bone.data.scaleY) * alpha; + pose.scaleX += (x - setup.scaleX) * alpha; + pose.scaleY += (y - setup.scaleY) * alpha; } } else { switch (blend) { case MixBlend.setup: - bx = Math.abs(bone.data.scaleX) * MathUtils.signum(x); - by = Math.abs(bone.data.scaleY) * MathUtils.signum(y); - bone.scaleX = bx + (x - bx) * alpha; - bone.scaleY = by + (y - by) * alpha; + 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(bone.scaleX) * MathUtils.signum(x); - by = Math.abs(bone.scaleY) * MathUtils.signum(y); - bone.scaleX = bx + (x - bx) * alpha; - bone.scaleY = by + (y - by) * alpha; + 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: - bone.scaleX += (x - bone.data.scaleX) * alpha; - bone.scaleY += (y - bone.data.scaleY) * alpha; + pose.scaleX += (x - setup.scaleX) * alpha; + pose.scaleY += (y - setup.scaleY) * alpha; } } } } } -/** Changes a bone's local {@link Bone#scaleX)} and {@link Bone#scaleY}. */ -export class ScaleXTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +/** Changes a bone's local {@link BoneLocal.scaleX}. */ +export class ScaleXTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.scaleX + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.scaleX); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.scaleX = this.getScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.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); } } -/** Changes a bone's local {@link Bone#scaleX)} and {@link Bone#scaleY}. */ -export class ScaleYTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +/** Changes a bone's local {@link BoneLocal.scaleY}. */ +export class ScaleYTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.scaleY + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.scaleY); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.scaleY = this.getScaleValue(time, alpha, blend, direction, bone.scaleY, bone.data.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); } } /** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ -export class ShearTimeline extends CurveTimeline2 implements BoneTimeline { - boneIndex = 0; - +export class ShearTimeline extends BoneTimeline2 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, - Property.shearX + "|" + boneIndex, - Property.shearY + "|" + boneIndex - ); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.shearX, Property.shearY); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (!bone.active) return; - + protected apply1 (pose: BoneLocal, setup: BoneLocal, time: number, alpha:number, blend: MixBlend,direction: MixDirection) { let frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.setup: - bone.shearX = bone.data.shearX; - bone.shearY = bone.data.shearY; + pose.shearX = setup.shearX; + pose.shearY = setup.shearY; return; case MixBlend.first: - bone.shearX += (bone.data.shearX - bone.shearX) * alpha; - bone.shearY += (bone.data.shearY - bone.shearY) * alpha; + pose.shearX += (setup.shearX - pose.shearX) * alpha; + pose.shearY += (setup.shearY - pose.shearY) * alpha; } return; } @@ -765,56 +832,49 @@ export class ShearTimeline extends CurveTimeline2 implements BoneTimeline { switch (blend) { case MixBlend.setup: - bone.shearX = bone.data.shearX + x * alpha; - bone.shearY = bone.data.shearY + y * alpha; + pose.shearX = setup.shearX + x * alpha; + pose.shearY = setup.shearY + y * alpha; break; case MixBlend.first: case MixBlend.replace: - bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; - bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; + pose.shearX += (setup.shearX + x - pose.shearX) * alpha; + pose.shearY += (setup.shearY + y - pose.shearY) * alpha; break; case MixBlend.add: - bone.shearX += x * alpha; - bone.shearY += y * alpha; + pose.shearX += x * alpha; + pose.shearY += y * alpha; } } } /** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ -export class ShearXTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +export class ShearXTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.shearX + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.shearX); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.shearX = this.getRelativeValue(time, alpha, blend, bone.shearX, bone.data.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); } } /** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ -export class ShearYTimeline extends CurveTimeline1 implements BoneTimeline { - boneIndex = 0; - +export class ShearYTimeline extends BoneTimeline1 { constructor (frameCount: number, bezierCount: number, boneIndex: number) { - super(frameCount, bezierCount, Property.shearY + "|" + boneIndex); - this.boneIndex = boneIndex; + super(frameCount, bezierCount, boneIndex, Property.shearY); } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let bone = skeleton.bones[this.boneIndex]; - if (bone.active) bone.shearY = this.getRelativeValue(time, alpha, blend, bone.shearY, bone.data.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); } } +/** Changes a bone's {@link BoneLocal.inherit}. */ export class InheritTimeline extends Timeline implements BoneTimeline { - boneIndex = 0; + readonly boneIndex: number; constructor (frameCount: number, boneIndex: number) { - super(frameCount, [Property.inherit + "|" + boneIndex]); + super(frameCount, Property.inherit + "|" + boneIndex); this.boneIndex = boneIndex; } @@ -822,7 +882,7 @@ export class InheritTimeline extends Timeline implements BoneTimeline { return 2/*ENTRIES*/; } - /** Sets the transform mode for the specified frame. + /** Sets the inherit transform mode for the specified frame. * @param frame Between 0 and frameCount, inclusive. * @param time The frame time in seconds. */ public setFrame (frame: number, time: number, inherit: Inherit) { @@ -831,36 +891,52 @@ 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) { + public apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + let bone = skeleton.bones[this.boneIndex]; if (!bone.active) return; + const pose = appliedPose ? bone.applied : bone.pose; - if (direction == MixDirection.mixOut) { - if (blend == MixBlend.setup) bone.inherit = bone.data.inherit; + if (direction === MixDirection.out) { + if (blend === MixBlend.setup) pose.inherit = bone.data.setup.inherit; return; } let frames = this.frames; if (time < frames[0]) { - if (blend == MixBlend.setup || blend == MixBlend.first) bone.inherit = bone.data.inherit; - return; - } - bone.inherit = this.frames[Timeline.search(frames, time, 2/*ENTRIES*/) + 1/*INHERIT*/]; + 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*/]; } } -/** Changes a slot's {@link Slot#color}. */ -export class RGBATimeline extends CurveTimeline implements SlotTimeline { - slotIndex = 0; +export abstract class SlotCurveTimeline extends CurveTimeline implements SlotTimeline { + readonly slotIndex: number; - constructor (frameCount: number, bezierCount: number, slotIndex: number) { - super(frameCount, bezierCount, [ - Property.rgb + "|" + slotIndex, - Property.alpha + "|" + slotIndex - ]); + constructor (frameCount: number, bezierCount: number, slotIndex: number, ...propertyIds: string[]) { + super(frameCount, bezierCount, ...propertyIds); this.slotIndex = slotIndex; } + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const slot = skeleton.slots[this.slotIndex]; + if (slot.bone.active) this.apply1(slot, appliedPose ? slot.applied : slot.pose, time, alpha, blend); + } + + protected abstract apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend): void; +} + +/** Changes a slot's {@link SlotPose.color}. */ +export class RGBATimeline extends SlotCurveTimeline { + constructor (frameCount: number, bezierCount: number, slotIndex: number) { + super(frameCount, bezierCount, slotIndex, // + Property.rgb + "|" + slotIndex, // + Property.alpha + "|" + slotIndex); + } + getFrameEntries () { return 5/*ENTRIES*/; } @@ -875,21 +951,15 @@ export class RGBATimeline extends CurveTimeline implements SlotTimeline { this.frames[frame + 4/*A*/] = a; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let slot = skeleton.slots[this.slotIndex]; - if (!slot.bone.active) return; - + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { let frames = this.frames; - let color = slot.color; + let color = pose.color; if (time < frames[0]) { - let setup = slot.data.color; + let setup = slot.data.setup.color; switch (blend) { - case MixBlend.setup: - color.setFromColor(setup); - return; - 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); + 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; } return; } @@ -925,21 +995,16 @@ export class RGBATimeline extends CurveTimeline implements SlotTimeline { if (alpha == 1) color.set(r, g, b, a); else { - if (blend == MixBlend.setup) color.setFromColor(slot.data.color); + if (blend == MixBlend.setup) color.setFromColor(slot.data.setup.color); color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha); } } } -/** Changes a slot's {@link Slot#color}. */ -export class RGBTimeline extends CurveTimeline implements SlotTimeline { - slotIndex = 0; - +/** Changes the RGB for a slot's {@link SlotPose.color}. */ +export class RGBTimeline extends SlotCurveTimeline { constructor (frameCount: number, bezierCount: number, slotIndex: number) { - super(frameCount, bezierCount, [ - Property.rgb + "|" + slotIndex - ]); - this.slotIndex = slotIndex; + super(frameCount, bezierCount, slotIndex, Property.rgb + "|" + slotIndex); } getFrameEntries () { @@ -955,14 +1020,11 @@ export class RGBTimeline extends CurveTimeline implements SlotTimeline { this.frames[frame + 3/*B*/] = b; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let slot = skeleton.slots[this.slotIndex]; - if (!slot.bone.active) return; - + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { let frames = this.frames; - let color = slot.color; + let color = pose.color; if (time < frames[0]) { - let setup = slot.data.color; + let setup = slot.data.setup.color; switch (blend) { case MixBlend.setup: color.r = setup.r; @@ -1007,7 +1069,7 @@ export class RGBTimeline extends CurveTimeline implements SlotTimeline { color.b = b; } else { if (blend == MixBlend.setup) { - let setup = slot.data.color; + let setup = slot.data.setup.color; color.r = setup.r; color.g = setup.g; color.b = setup.b; @@ -1019,7 +1081,7 @@ export class RGBTimeline extends CurveTimeline implements SlotTimeline { } } -/** Changes a bone's local {@link Bone#shearX} and {@link Bone#shearY}. */ +/** Changes the alpha for a slot's {@link SlotPose.color}. */ export class AlphaTimeline extends CurveTimeline1 implements SlotTimeline { slotIndex = 0; @@ -1028,19 +1090,19 @@ 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) { + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + let slot = skeleton.slots[this.slotIndex]; if (!slot.bone.active) return; - let color = slot.color; + const color = (appliedPose ? slot.applied : slot.pose).color; + const frames = this.frames; if (time < this.frames[0]) { - let setup = slot.data.color; + let setup = slot.data.setup.color; switch (blend) { - case MixBlend.setup: - color.a = setup.a; - return; - case MixBlend.first: - color.a += (setup.a - color.a) * alpha; + case MixBlend.setup: color.a = setup.a; break; + case MixBlend.first: color.a += (setup.a - color.a) * alpha; break; } return; } @@ -1049,23 +1111,19 @@ export class AlphaTimeline extends CurveTimeline1 implements SlotTimeline { if (alpha == 1) color.a = a; else { - if (blend == MixBlend.setup) color.a = slot.data.color.a; + if (blend == MixBlend.setup) color.a = slot.data.setup.color.a; color.a += (a - color.a) * alpha; } } } -/** Changes a slot's {@link Slot#color} and {@link Slot#darkColor} for two color tinting. */ -export class RGBA2Timeline extends CurveTimeline implements SlotTimeline { - slotIndex = 0; - +/** Changes a slot's {@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, [ - Property.rgb + "|" + slotIndex, - Property.alpha + "|" + slotIndex, - Property.rgb2 + "|" + slotIndex - ]); - this.slotIndex = slotIndex; + super(frameCount, bezierCount, slotIndex, // + Property.rgb + "|" + slotIndex, // + Property.alpha + "|" + slotIndex, // + Property.rgb2 + "|" + slotIndex); } getFrameEntries () { @@ -1085,14 +1143,12 @@ export class RGBA2Timeline extends CurveTimeline implements SlotTimeline { this.frames[frame + 7/*B2*/] = b2; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let slot = skeleton.slots[this.slotIndex]; - if (!slot.bone.active) return; - - let frames = this.frames; - let light = slot.color, dark = slot.darkColor!; + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + const frames = this.frames; + const light = pose.color, dark = pose.darkColor!; if (time < frames[0]) { - let setupLight = slot.data.color, setupDark = slot.data.darkColor!; + const setup = slot.data.setup; + const setupLight = setup.color, setupDark = setup.darkColor!; switch (blend) { case MixBlend.setup: light.setFromColor(setupLight); @@ -1158,8 +1214,9 @@ export class RGBA2Timeline extends CurveTimeline implements SlotTimeline { dark.b = b2; } else { if (blend == MixBlend.setup) { - light.setFromColor(slot.data.color); - let setupDark = slot.data.darkColor!; + const setup = slot.data.setup; + light.setFromColor(setup.color); + let setupDark = setup.darkColor!; dark.r = setupDark.r; dark.g = setupDark.g; dark.b = setupDark.b; @@ -1172,16 +1229,12 @@ export class RGBA2Timeline extends CurveTimeline implements SlotTimeline { } } -/** Changes a slot's {@link Slot#color} and {@link Slot#darkColor} for two color tinting. */ -export class RGB2Timeline extends CurveTimeline implements SlotTimeline { - slotIndex = 0; - +/** Changes a slot's {@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, [ - Property.rgb + "|" + slotIndex, - Property.rgb2 + "|" + slotIndex - ]); - this.slotIndex = slotIndex; + super(frameCount, bezierCount, slotIndex, // + Property.rgb + "|" + slotIndex, // + Property.rgb2 + "|" + slotIndex); } getFrameEntries () { @@ -1200,14 +1253,12 @@ export class RGB2Timeline extends CurveTimeline implements SlotTimeline { this.frames[frame + 6/*B2*/] = b2; } - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let slot = skeleton.slots[this.slotIndex]; - if (!slot.bone.active) return; - - let frames = this.frames; - let light = slot.color, dark = slot.darkColor!; + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + const frames = this.frames; + const light = pose.color, dark = pose.darkColor!; if (time < frames[0]) { - let setupLight = slot.data.color, setupDark = slot.data.darkColor!; + const setup = slot.data.setup; + const setupLight = setup.color, setupDark = setup.darkColor!; switch (blend) { case MixBlend.setup: light.r = setupLight.r; @@ -1274,7 +1325,8 @@ export class RGB2Timeline extends CurveTimeline implements SlotTimeline { dark.b = b2; } else { if (blend == MixBlend.setup) { - let setupLight = slot.data.color, setupDark = slot.data.darkColor!; + const setup = slot.data.setup; + const setupLight = setup.color, setupDark = setup.darkColor!; light.r = setupLight.r; light.g = setupLight.g; light.b = setupLight.b; @@ -1292,7 +1344,7 @@ export class RGB2Timeline extends CurveTimeline implements SlotTimeline { } } -/** Changes a slot's {@link Slot#attachment}. */ +/** Changes a slot's {@link SlotPose.ttachment}. */ export class AttachmentTimeline extends Timeline implements SlotTimeline { slotIndex = 0; @@ -1300,9 +1352,7 @@ export class AttachmentTimeline extends Timeline implements SlotTimeline { attachmentNames: Array; constructor (frameCount: number, slotIndex: number) { - super(frameCount, [ - Property.attachment + "|" + slotIndex - ]); + super(frameCount, Property.attachment + "|" + slotIndex); this.slotIndex = slotIndex; this.attachmentNames = new Array(frameCount); } @@ -1317,43 +1367,38 @@ 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) { + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + let slot = skeleton.slots[this.slotIndex]; if (!slot.bone.active) return; + const pose = appliedPose ? slot.applied : slot.pose; - if (direction == MixDirection.mixOut) { - if (blend == MixBlend.setup) this.setAttachment(skeleton, slot, slot.data.attachmentName); - return; - } - - if (time < this.frames[0]) { - if (blend == MixBlend.setup || blend == MixBlend.first) this.setAttachment(skeleton, slot, slot.data.attachmentName); - return; - } - - this.setAttachment(skeleton, slot, this.attachmentNames[Timeline.search1(this.frames, time)]); + 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); + } else + this.setAttachment(skeleton, pose, this.attachmentNames[Timeline.search(this.frames, time)]); } - setAttachment (skeleton: Skeleton, slot: Slot, attachmentName: string | null) { - slot.setAttachment(!attachmentName ? null : skeleton.getAttachment(this.slotIndex, attachmentName)); + setAttachment (skeleton: Skeleton, pose: SlotPose, attachmentName: string | null) { + pose.setAttachment(!attachmentName ? null : skeleton.getAttachment(this.slotIndex, attachmentName)); } } -/** Changes a slot's {@link Slot#deform} to deform a {@link VertexAttachment}. */ -export class DeformTimeline extends CurveTimeline implements SlotTimeline { - slotIndex = 0; - - /** The attachment that will be deformed. */ - attachment: VertexAttachment; +/** Changes a slot's {@link SlotPose.deform} to deform a {@link VertexAttachment}. */ +export class DeformTimeline extends SlotCurveTimeline { + /** The attachment that will be deformed. + * + * See {@link VertexAttachment.getTimelineAttachment()}. */ + readonly attachment: VertexAttachment; /** The vertices for each key frame. */ vertices: Array; constructor (frameCount: number, bezierCount: number, slotIndex: number, attachment: VertexAttachment) { - super(frameCount, bezierCount, [ - Property.deform + "|" + slotIndex + "|" + attachment.id - ]); - this.slotIndex = slotIndex; + super(frameCount, bezierCount, slotIndex, Property.deform + "|" + slotIndex + "|" + attachment.id); this.attachment = attachment; this.vertices = new Array(frameCount); } @@ -1362,7 +1407,9 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { return this.frames.length; } - /** Sets the time in seconds and the vertices for the specified key frame. + /** Sets the time and vertices for the specified frame. + * @param frame Between 0 and frameCount, inclusive. + * @param time The frame time in seconds. * @param vertices Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. */ setFrame (frame: number, time: number, vertices: NumberArrayLike) { this.frames[frame] = time; @@ -1419,13 +1466,12 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { return y + (1 - y) * (time - x) / (this.frames[frame + this.getFrameEntries()] - x); } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let slot: Slot = skeleton.slots[this.slotIndex]; - if (!slot.bone.active || !(slot.attachment instanceof VertexAttachment)) return; - let vertexAttachment = slot.attachment; + protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) { + if (!(pose.attachment instanceof VertexAttachment)) return; + let vertexAttachment = pose.attachment; if (vertexAttachment.timelineAttachment != this.attachment) return; - let deform: Array = slot.deform; + let deform = pose.deform; if (deform.length == 0) blend = MixBlend.setup; let vertices = this.vertices; @@ -1443,13 +1489,11 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { return; } deform.length = vertexCount; - if (!vertexAttachment.bones) { - // Unweighted vertex positions. + if (!vertexAttachment.bones) { // Unweighted vertex positions. let setupVertices = vertexAttachment.vertices; for (var i = 0; i < vertexCount; i++) deform[i] += (setupVertices[i] - deform[i]) * alpha; - } else { - // Weighted deform offsets. + } else { // Weighted deform offsets. alpha = 1 - alpha; for (var i = 0; i < vertexCount; i++) deform[i] *= alpha; @@ -1459,52 +1503,46 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { } deform.length = vertexCount; - if (time >= frames[frames.length - 1]) { + if (time >= frames[frames.length - 1]) { // Time is after last frame. let lastVertices = vertices[frames.length - 1]; if (alpha == 1) { if (blend == MixBlend.add) { - if (!vertexAttachment.bones) { - // Unweighted vertex positions, with alpha. + if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. let setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i] - setupVertices[i]; - } else { - // Weighted deform offsets, with alpha. + } else { // Weighted deform offsets, no alpha. for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i]; } - } else + } 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. + if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. let setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) { let setup = setupVertices[i]; deform[i] = setup + (lastVertices[i] - setup) * alpha; } - } else { - // Weighted deform offsets, with alpha. + } else { // Weighted deform offsets, with alpha. for (let i = 0; i < vertexCount; i++) deform[i] = lastVertices[i] * alpha; } break; } case MixBlend.first: - case MixBlend.replace: + 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, with alpha. + if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. let setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; - } else { - // Weighted deform offsets, with alpha. + } else { // Weighted deform offsets, alpha. for (let i = 0; i < vertexCount; i++) deform[i] += lastVertices[i] * alpha; } @@ -1513,29 +1551,28 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { return; } - // Interpolate between the previous frame and the current frame. - let frame = Timeline.search1(frames, time); + let frame = Timeline.search(frames, time); let percent = this.getCurvePercent(time, frame); let prevVertices = vertices[frame]; let nextVertices = vertices[frame + 1]; if (alpha == 1) { if (blend == MixBlend.add) { - if (!vertexAttachment.bones) { - // Unweighted vertex positions, with alpha. + if (!vertexAttachment.bones) { // Unweighted vertex positions, no alpha. let setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i]; deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; } - } else { - // Weighted deform offsets, with alpha. + } else { // Weighted deform offsets, no alpha. for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i]; deform[i] += prev + (nextVertices[i] - prev) * percent; } } - } else { + } else if (percent === 0) + Utils.arrayCopy(prevVertices, 0, deform, 0, vertexCount) + else { // Vertex positions or deform offsets, no alpha. for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i]; deform[i] = prev + (nextVertices[i] - prev) * percent; @@ -1544,15 +1581,13 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { } else { switch (blend) { case MixBlend.setup: { - if (!vertexAttachment.bones) { - // Unweighted vertex positions, with alpha. + if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. let setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i], setup = setupVertices[i]; deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; } - } else { - // Weighted deform offsets, with alpha. + } else { // Weighted deform offsets, with alpha. for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i]; deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; @@ -1561,22 +1596,20 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { break; } case MixBlend.first: - case MixBlend.replace: + case MixBlend.replace: // Vertex positions or deform offsets, with alpha. for (let i = 0; i < vertexCount; i++) { let 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. + if (!vertexAttachment.bones) { // Unweighted vertex positions, with alpha. let setupVertices = vertexAttachment.vertices; for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i]; deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; } - } else { - // Weighted deform offsets, with alpha. + } else { // Weighted deform offsets, with alpha. for (let i = 0; i < vertexCount; i++) { let prev = prevVertices[i]; deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; @@ -1587,6 +1620,104 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline { } } +/** Changes a slot's {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */ +export class SequenceTimeline extends Timeline implements SlotTimeline { + static ENTRIES = 3; + static MODE = 1; + static DELAY = 2; + + readonly slotIndex: number; + readonly attachment: HasTextureRegion; + + constructor (frameCount: number, slotIndex: number, attachment: HasTextureRegion) { + super(frameCount, + Property.sequence + "|" + slotIndex + "|" + attachment.sequence!.id); + this.slotIndex = slotIndex; + this.attachment = attachment; + } + + getFrameEntries () { + return SequenceTimeline.ENTRIES; + } + + getSlotIndex () { + return this.slotIndex; + } + + getAttachment () { + return this.attachment as unknown as Attachment; + } + + /** Sets the time, mode, index, and frame time for the specified frame. + * @param frame Between 0 and frameCount, inclusive. + * @param time Seconds between frames. */ + setFrame (frame: number, time: number, mode: SequenceMode, index: number, delay: number) { + let frames = this.frames; + frame *= SequenceTimeline.ENTRIES; + frames[frame] = time; + frames[frame + SequenceTimeline.MODE] = mode | (index << 4); + frames[frame + SequenceTimeline.DELAY] = delay; + } + + apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + let slot = skeleton.slots[this.slotIndex]; + if (!slot.bone.active) return; + const pose = appliedPose ? slot.applied : slot.pose; + + let slotAttachment = pose.attachment; + let attachment = this.attachment as unknown as Attachment; + if (slotAttachment !== attachment) { + if (!(slotAttachment instanceof VertexAttachment) + || slotAttachment.timelineAttachment !== attachment) return; + } + + const sequence = (slotAttachment as unknown as HasTextureRegion).sequence; + if (!sequence) return; + + if (direction === MixDirection.out) { + if (blend === MixBlend.setup) pose.sequenceIndex = -1; + return; + } + + let frames = this.frames; + if (time < frames[0]) { + if (blend === MixBlend.setup || blend === MixBlend.first) pose.sequenceIndex = -1; + return; + } + + const i = Timeline.search(frames, time, SequenceTimeline.ENTRIES); + const before = frames[i]; + const modeAndIndex = frames[i + SequenceTimeline.MODE]; + const delay = frames[i + SequenceTimeline.DELAY]; + + let index = modeAndIndex >> 4, count = sequence.regions.length; + const mode = SequenceModeValues[modeAndIndex & 0xf]; + if (mode !== SequenceMode.hold) { + index += (((time - before) / delay + 0.00001) | 0); + switch (mode) { + case SequenceMode.once: index = Math.min(count - 1, index); break; + case SequenceMode.loop: index %= count; break; + case SequenceMode.pingpong: { + let n = (count << 1) - 2; + index = n === 0 ? 0 : index % n; + if (index >= count) index = n - index; + break; + } + case SequenceMode.onceReverse: index = Math.max(count - 1 - index, 0); break; + case SequenceMode.loopReverse: index = count - 1 - (index % count); break; + case SequenceMode.pingpongReverse: { + let n = (count << 1) - 2; + index = n == 0 ? 0 : (index + count - 1) % n; + if (index >= count) index = n - index; + } + } + } + pose.sequenceIndex = index; + } +} + /** Fires an {@link Event} when specific animation times are reached. */ export class EventTimeline extends Timeline { static propertyIds = ["" + Property.event]; @@ -1595,7 +1726,7 @@ export class EventTimeline extends Timeline { events: Array; constructor (frameCount: number) { - super(frameCount, EventTimeline.propertyIds); + super(frameCount, ...EventTimeline.propertyIds); this.events = new Array(frameCount); } @@ -1611,14 +1742,16 @@ 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) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, + blend: MixBlend, direction: MixDirection, appliedPose: boolean) { + if (!firedEvents) return; let frames = this.frames; let frameCount = this.frames.length; if (lastTime > time) { // Apply after lastTime for looped animations. - this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha, blend, direction); + this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha, blend, direction, appliedPose); lastTime = -1; } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. return; @@ -1628,7 +1761,7 @@ export class EventTimeline extends Timeline { if (lastTime < frames[0]) i = 0; else { - i = Timeline.search1(frames, lastTime) + 1; + i = Timeline.search(frames, lastTime) + 1; let frameTime = frames[i]; while (i > 0) { // Fire multiple events with the same frame. if (frames[i - 1] != frameTime) break; @@ -1648,7 +1781,7 @@ export class DrawOrderTimeline extends Timeline { drawOrders: Array | null>; constructor (frameCount: number) { - super(frameCount, DrawOrderTimeline.propertyIds); + super(frameCount, ...DrawOrderTimeline.propertyIds); this.drawOrders = new Array | null>(frameCount); } @@ -1664,8 +1797,10 @@ export class DrawOrderTimeline extends Timeline { this.drawOrders[frame] = drawOrder; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - if (direction == MixDirection.mixOut) { + 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); return; } @@ -1675,7 +1810,7 @@ export class DrawOrderTimeline extends Timeline { return; } - let idx = Timeline.search1(this.frames, time); + let idx = Timeline.search(this.frames, time); let drawOrderToSetupIndex = this.drawOrders[idx]; if (!drawOrderToSetupIndex) Utils.arrayCopy(skeleton.slots, 0, skeleton.drawOrder, 0, skeleton.slots.length); @@ -1688,24 +1823,34 @@ export class DrawOrderTimeline extends Timeline { } } -/** Changes an IK constraint's {@link IkConstraint#mix}, {@link IkConstraint#softness}, - * {@link IkConstraint#bendDirection}, {@link IkConstraint#stretch}, and {@link IkConstraint#compress}. */ -export class IkConstraintTimeline extends CurveTimeline { - /** The index of the IK constraint in {@link Skeleton#getIkConstraints()} that will be changed when this timeline is applied */ - constraintIndex: number = 0; +export interface ConstraintTimeline { + /** The index of the constraint in {@link Skeleton.constraints} that will be changed when this timeline is applied. */ + readonly constraintIndex: number; +} - constructor (frameCount: number, bezierCount: number, ikConstraintIndex: number) { - super(frameCount, bezierCount, [ - Property.ikConstraint + "|" + ikConstraintIndex - ]); - this.constraintIndex = ikConstraintIndex; +export function isConstraintTimeline(obj: any): obj is ConstraintTimeline { + return typeof obj === 'object' && obj !== null && typeof obj.constraintIndex === 'number'; +} + +/** Changes an IK constraint's {@link IkConstraintPose.mix)}, {@link IkConstraintPose.softness}, + * {@link IkConstraintPose.bendDirection}, {@link IkConstraintPose.stretch}, and + * {@link IkConstraintPose.compress}. */ +export class IkConstraintTimeline extends CurveTimeline implements ConstraintTimeline { + readonly constraintIndex: number = 0; + + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, Property.ikConstraint + "|" + constraintIndex); + this.constraintIndex = constraintIndex; } getFrameEntries () { return 6/*ENTRIES*/; } - /** Sets the time in seconds, mix, softness, bend direction, compress, and stretch for the specified key frame. */ + /** Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame. + * @param frame Between 0 and frameCount, inclusive. + * @param time The frame time in seconds. + * @param bendDirection 1 or -1. */ setFrame (frame: number, time: number, mix: number, softness: number, bendDirection: number, compress: boolean, stretch: boolean) { frame *= 6/*ENTRIES*/; this.frames[frame] = time; @@ -1716,26 +1861,30 @@ export class IkConstraintTimeline extends CurveTimeline { this.frames[frame + 5/*STRETCH*/] = stretch ? 1 : 0; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let constraint: IkConstraint = skeleton.ikConstraints[this.constraintIndex]; + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const constraint = skeleton.constraints[this.constraintIndex]; if (!constraint.active) return; + const pose = appliedPose ? constraint.applied : constraint.pose; let frames = this.frames; if (time < frames[0]) { + const setup = constraint.data.setup; switch (blend) { case MixBlend.setup: - constraint.mix = constraint.data.mix; - constraint.softness = constraint.data.softness; - constraint.bendDirection = constraint.data.bendDirection; - constraint.compress = constraint.data.compress; - constraint.stretch = constraint.data.stretch; + pose.mix = setup.mix; + pose.softness = setup.softness; + pose.bendDirection = setup.bendDirection; + pose.compress = setup.compress; + pose.stretch = setup.stretch; return; case MixBlend.first: - constraint.mix += (constraint.data.mix - constraint.mix) * alpha; - constraint.softness += (constraint.data.softness - constraint.softness) * alpha; - constraint.bendDirection = constraint.data.bendDirection; - constraint.compress = constraint.data.compress; - constraint.stretch = constraint.data.stretch; + 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; } return; } @@ -1761,49 +1910,55 @@ export class IkConstraintTimeline extends CurveTimeline { softness = this.getBezierValue(time, i, 2/*SOFTNESS*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/); } - if (blend == MixBlend.setup) { - constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha; - constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha; - - if (direction == MixDirection.mixOut) { - constraint.bendDirection = constraint.data.bendDirection; - constraint.compress = constraint.data.compress; - constraint.stretch = constraint.data.stretch; - } else { - constraint.bendDirection = frames[i + 3/*BEND_DIRECTION*/]; - constraint.compress = frames[i + 4/*COMPRESS*/] != 0; - constraint.stretch = frames[i + 5/*STRETCH*/] != 0; - } - } else { - constraint.mix += (mix - constraint.mix) * alpha; - constraint.softness += (softness - constraint.softness) * alpha; - if (direction == MixDirection.mixIn) { - constraint.bendDirection = frames[i + 3/*BEND_DIRECTION*/]; - constraint.compress = frames[i + 4/*COMPRESS*/] != 0; - constraint.stretch = frames[i + 5/*STRETCH*/] != 0; - } + switch (blend) { + case 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; + } + break; + case MixBlend.first: + case MixBlend.replace: + pose.mix += (mix - pose.mix) * alpha; + pose.softness += (softness - pose.softness) * alpha; + if (direction === MixDirection.out) return; + break; + case MixBlend.add: + pose.mix += mix * alpha; + pose.softness += softness * alpha; + if (direction === MixDirection.out) return; + break; } + 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 TransformConstraint#rotateMix}, {@link TransformConstraint#translateMix}, - * {@link TransformConstraint#scaleMix}, and {@link TransformConstraint#shearMix}. */ -export class TransformConstraintTimeline extends CurveTimeline { - /** The index of the transform constraint slot in {@link Skeleton#transformConstraints} that will be changed. */ +/** Changes a transform constraint's {@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 { + /** The index of the transform constraint slot in {@link Skeleton.transformConstraints} that will be changed. */ constraintIndex: number = 0; - constructor (frameCount: number, bezierCount: number, transformConstraintIndex: number) { - super(frameCount, bezierCount, [ - Property.transformConstraint + "|" + transformConstraintIndex - ]); - this.constraintIndex = transformConstraintIndex; + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, Property.transformConstraint + "|" + constraintIndex); + this.constraintIndex = constraintIndex; } getFrameEntries () { return 7/*ENTRIES*/; } - /** The time in seconds, rotate mix, translate mix, scale mix, and shear mix for the specified key frame. */ + /** Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame. + * @param frame Between 0 and frameCount, inclusive. + * @param time The frame time in seconds. */ setFrame (frame: number, time: number, mixRotate: number, mixX: number, mixY: number, mixScaleX: number, mixScaleY: number, mixShearY: number) { let frames = this.frames; @@ -1817,29 +1972,32 @@ export class TransformConstraintTimeline extends CurveTimeline { frames[frame + 6/*SHEARY*/] = mixShearY; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let constraint: TransformConstraint = skeleton.transformConstraints[this.constraintIndex]; + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const constraint = skeleton.constraints[this.constraintIndex]; if (!constraint.active) return; + const pose = appliedPose ? constraint.applied : constraint.pose; let frames = this.frames; if (time < frames[0]) { - let data = constraint.data; + const setup = constraint.data.setup; switch (blend) { case MixBlend.setup: - constraint.mixRotate = data.mixRotate; - constraint.mixX = data.mixX; - constraint.mixY = data.mixY; - constraint.mixScaleX = data.mixScaleX; - constraint.mixScaleY = data.mixScaleY; - constraint.mixShearY = data.mixShearY; + 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: - constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha; - constraint.mixX += (data.mixX - constraint.mixX) * alpha; - constraint.mixY += (data.mixY - constraint.mixY) * alpha; - constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha; - constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha; - constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha; + 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; } return; } @@ -1881,79 +2039,97 @@ export class TransformConstraintTimeline extends CurveTimeline { shearY = this.getBezierValue(time, i, 6/*SHEARY*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/); } - if (blend == MixBlend.setup) { - let data = constraint.data; - constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; - constraint.mixX = data.mixX + (x - data.mixX) * alpha; - constraint.mixY = data.mixY + (y - data.mixY) * alpha; - constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha; - constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha; - constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha; - } else { - constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; - constraint.mixX += (x - constraint.mixX) * alpha; - constraint.mixY += (y - constraint.mixY) * alpha; - constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha; - constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha; - constraint.mixShearY += (shearY - constraint.mixShearY) * alpha; + 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; } } } -/** Changes a path constraint's {@link PathConstraint#position}. */ -export class PathConstraintPositionTimeline extends CurveTimeline1 { - /** The index of the path constraint in {@link Skeleton#getPathConstraints()} that will be changed when this timeline is - * applied. */ - constraintIndex: number = 0; +export abstract class ConstraintTimeline1 extends CurveTimeline1 implements ConstraintTimeline { + readonly constraintIndex: number; - constructor (frameCount: number, bezierCount: number, pathConstraintIndex: number) { - super(frameCount, bezierCount, Property.pathConstraintPosition + "|" + pathConstraintIndex); - this.constraintIndex = pathConstraintIndex; - } - - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let constraint: PathConstraint = skeleton.pathConstraints[this.constraintIndex]; - if (constraint.active) - constraint.position = this.getAbsoluteValue(time, alpha, blend, constraint.position, constraint.data.position); + constructor (frameCount: number, bezierCount: number, constraintIndex: number, property: Property) { + super(frameCount, bezierCount, property + "|" + constraintIndex); + this.constraintIndex = constraintIndex; } } -/** Changes a path constraint's {@link PathConstraint#spacing}. */ -export class PathConstraintSpacingTimeline extends CurveTimeline1 { - /** The index of the path constraint in {@link Skeleton#getPathConstraints()} that will be changed when this timeline is - * applied. */ - constraintIndex = 0; - - constructor (frameCount: number, bezierCount: number, pathConstraintIndex: number) { - super(frameCount, bezierCount, Property.pathConstraintSpacing + "|" + pathConstraintIndex); - this.constraintIndex = pathConstraintIndex; +/** Changes a path constraint's {@link PathConstraintPose.position}. */ +export class PathConstraintPositionTimeline extends ConstraintTimeline1 { + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.pathConstraintPosition); } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let constraint: PathConstraint = skeleton.pathConstraints[this.constraintIndex]; - if (constraint.active) - constraint.spacing = this.getAbsoluteValue(time, alpha, blend, constraint.spacing, constraint.data.spacing); + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const constraint = skeleton.constraints[this.constraintIndex]; + if (constraint.active) { + const pose = appliedPose ? constraint.applied : constraint.pose; + pose.position = this.getAbsoluteValue(time, alpha, blend, pose.position, constraint.data.setup.position); + } } } -/** Changes a transform constraint's {@link PathConstraint#getMixRotate()}, {@link PathConstraint#getMixX()}, and - * {@link PathConstraint#getMixY()}. */ -export class PathConstraintMixTimeline extends CurveTimeline { - /** The index of the path constraint in {@link Skeleton#getPathConstraints()} that will be changed when this timeline is - * applied. */ - constraintIndex = 0; +/** Changes a path constraint's {@link PathConstraintPose.spacing}. */ +export class PathConstraintSpacingTimeline extends ConstraintTimeline1 { + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.pathConstraintSpacing); + } - constructor (frameCount: number, bezierCount: number, pathConstraintIndex: number) { - super(frameCount, bezierCount, [ - Property.pathConstraintMix + "|" + pathConstraintIndex - ]); - this.constraintIndex = pathConstraintIndex; + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const constraint = skeleton.constraints[this.constraintIndex]; + if (constraint.active) { + const pose = appliedPose ? constraint.applied : constraint.pose; + pose.spacing = this.getAbsoluteValue(time, alpha, blend, pose.spacing, constraint.data.setup.spacing); + } + } +} + +/** Changes a transform constraint's {@link PathConstraint.mixRotate()}, {@link PathConstraint.mixX()}, and + * {@link PathConstraint.mixY()}. */ +export class PathConstraintMixTimeline extends CurveTimeline implements ConstraintTimeline { + readonly constraintIndex: number; + + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, Property.pathConstraintMix + "|" + constraintIndex); + this.constraintIndex = constraintIndex; } getFrameEntries () { return 4/*ENTRIES*/; } + /** Sets the time and color for the specified frame. + * @param frame Between 0 and frameCount, inclusive. + * @param time The frame time in seconds. */ setFrame (frame: number, time: number, mixRotate: number, mixX: number, mixY: number) { let frames = this.frames; frame <<= 2; @@ -1963,22 +2139,26 @@ export class PathConstraintMixTimeline extends CurveTimeline { frames[frame + 3/*Y*/] = mixY; } - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let constraint: PathConstraint = skeleton.pathConstraints[this.constraintIndex]; + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const constraint = skeleton.constraints[this.constraintIndex]; if (!constraint.active) return; + const pose = appliedPose ? constraint.applied : constraint.pose; let frames = this.frames; if (time < frames[0]) { + const setup = constraint.data.setup; switch (blend) { case MixBlend.setup: - constraint.mixRotate = constraint.data.mixRotate; - constraint.mixX = constraint.data.mixX; - constraint.mixY = constraint.data.mixY; + pose.mixRotate = setup.mixRotate; + pose.mixX = setup.mixX; + pose.mixY = setup.mixY; return; case MixBlend.first: - constraint.mixRotate += (constraint.data.mixRotate - constraint.mixRotate) * alpha; - constraint.mixX += (constraint.data.mixX - constraint.mixX) * alpha; - constraint.mixY += (constraint.data.mixY - constraint.mixY) * alpha; + pose.mixRotate += (setup.mixRotate - pose.mixRotate) * alpha; + pose.mixX += (setup.mixX - pose.mixX) * alpha; + pose.mixY += (setup.mixY - pose.mixY) * alpha; } return; } @@ -2008,71 +2188,75 @@ export class PathConstraintMixTimeline extends CurveTimeline { y = this.getBezierValue(time, i, 3/*Y*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/); } - if (blend == MixBlend.setup) { - let data = constraint.data; - constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; - constraint.mixX = data.mixX + (x - data.mixX) * alpha; - constraint.mixY = data.mixY + (y - data.mixY) * alpha; - } else { - constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; - constraint.mixX += (x - constraint.mixX) * alpha; - constraint.mixY += (y - constraint.mixY) * alpha; + 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; + 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; + break; + case MixBlend.add: + pose.mixRotate += rotate * alpha; + pose.mixX += x * alpha; + pose.mixY += y * alpha; + break; } } } /** The base class for most {@link PhysicsConstraint} timelines. */ -export abstract class PhysicsConstraintTimeline extends CurveTimeline1 { - /** The index of the physics constraint in {@link Skeleton#getPhysicsConstraints()} that will be changed when this timeline - * is applied, or -1 if all physics constraints in the skeleton will be changed. */ - constraintIndex = 0; - - /** @param physicsConstraintIndex -1 for all physics constraints in the skeleton. */ - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) { - super(frameCount, bezierCount, property + "|" + physicsConstraintIndex); - this.constraintIndex = physicsConstraintIndex; +export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 { + /** @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) { - let constraint: PhysicsConstraint; + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + if (this.constraintIndex == -1) { const value = time >= this.frames[0] ? this.getCurveValue(time) : 0; - - for (const constraint of skeleton.physicsConstraints) { - if (constraint.active && this.global(constraint.data)) - this.set(constraint, this.getAbsoluteValue2(time, alpha, blend, this.get(constraint), this.setup(constraint), value)); + 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)); + } } } else { - constraint = skeleton.physicsConstraints[this.constraintIndex]; - if (constraint.active) this.set(constraint, this.getAbsoluteValue(time, alpha, blend, this.get(constraint), this.setup(constraint))); + const constraint = skeleton.constraints[this.constraintIndex]; + 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))); + } } } - abstract setup (constraint: PhysicsConstraint): number; + abstract get (pose: PhysicsConstraintPose): number; - abstract get (constraint: PhysicsConstraint): number; - - abstract set (constraint: PhysicsConstraint, value: number): void; + abstract set (pose: PhysicsConstraintPose, value: number): void; abstract global (constraint: PhysicsConstraintData): boolean; } -/** Changes a physics constraint's {@link PhysicsConstraint#getInertia()}. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.inertia}. */ export class PhysicsConstraintInertiaTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintInertia); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintInertia); } - setup (constraint: PhysicsConstraint): number { - return constraint.data.inertia; + get (pose: PhysicsConstraintPose): number { + return pose.inertia; } - get (constraint: PhysicsConstraint): number { - return constraint.inertia; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.inertia = value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.inertia = value; } global (constraint: PhysicsConstraintData): boolean { @@ -2080,22 +2264,17 @@ export class PhysicsConstraintInertiaTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraint#getStrength()}. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.strength}. */ export class PhysicsConstraintStrengthTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintStrength); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintStrength); + } + get (pose: PhysicsConstraintPose): number { + return pose.strength; } - setup (constraint: PhysicsConstraint): number { - return constraint.data.strength; - } - - get (constraint: PhysicsConstraint): number { - return constraint.strength; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.strength = value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.strength = value; } global (constraint: PhysicsConstraintData): boolean { @@ -2103,22 +2282,18 @@ export class PhysicsConstraintStrengthTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraint#getDamping()}. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.damping}. */ export class PhysicsConstraintDampingTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintDamping); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintDamping); } - setup (constraint: PhysicsConstraint): number { - return constraint.data.damping; + get (pose: PhysicsConstraintPose): number { + return pose.damping; } - get (constraint: PhysicsConstraint): number { - return constraint.damping; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.damping = value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.damping = value; } global (constraint: PhysicsConstraintData): boolean { @@ -2126,22 +2301,18 @@ export class PhysicsConstraintDampingTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraint#getMassInverse()}. The timeline values are not inverted. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.massInverse}. The timeline values are not inverted. */ export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintMass); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintMass); } - setup (constraint: PhysicsConstraint): number { - return 1 / constraint.data.massInverse; + get (pose: PhysicsConstraintPose): number { + return 1 / pose.massInverse; } - get (constraint: PhysicsConstraint): number { - return 1 / constraint.massInverse; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.massInverse = 1 / value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.massInverse = 1 / value; } global (constraint: PhysicsConstraintData): boolean { @@ -2149,22 +2320,18 @@ export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline { } } -/** Changes a physics constraint's {@link PhysicsConstraint#getWind()}. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.wind}. */ export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintWind); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind); } - setup (constraint: PhysicsConstraint): number { - return constraint.data.wind; + get (pose: PhysicsConstraintPose): number { + return pose.wind; } - get (constraint: PhysicsConstraint): number { - return constraint.wind; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.wind = value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.wind = value; } global (constraint: PhysicsConstraintData): boolean { @@ -2172,22 +2339,18 @@ export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline { } } -/** Changes a physics constraint's {@link PhysicsConstraint#getGravity()}. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.gravity}. */ export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintGravity); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity); } - setup (constraint: PhysicsConstraint): number { - return constraint.data.gravity; + get (pose: PhysicsConstraintPose): number { + return pose.gravity; } - get (constraint: PhysicsConstraint): number { - return constraint.gravity; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.gravity = value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.gravity = value; } global (constraint: PhysicsConstraintData): boolean { @@ -2195,22 +2358,18 @@ export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline } } -/** Changes a physics constraint's {@link PhysicsConstraint#getMix()}. */ +/** Changes a physics constraint's {@link PhysicsConstraintPose.mix}. */ export class PhysicsConstraintMixTimeline extends PhysicsConstraintTimeline { - constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number) { - super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintMix); + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintMix); } - setup (constraint: PhysicsConstraint): number { - return constraint.data.mix; + get (pose: PhysicsConstraintPose): number { + return pose.mix; } - get (constraint: PhysicsConstraint): number { - return constraint.mix; - } - - set (constraint: PhysicsConstraint, value: number): void { - constraint.mix = value; + set (pose: PhysicsConstraintPose, value: number): void { + pose.mix = value; } global (constraint: PhysicsConstraintData): boolean { @@ -2219,17 +2378,17 @@ export class PhysicsConstraintMixTimeline extends PhysicsConstraintTimeline { } /** Resets a physics constraint when specific animation times are reached. */ -export class PhysicsConstraintResetTimeline extends Timeline { +export class PhysicsConstraintResetTimeline extends Timeline implements ConstraintTimeline { private static propertyIds: string[] = [Property.physicsConstraintReset.toString()]; - /** The index of the physics constraint in {@link Skeleton#getPhysicsConstraints()} that will be reset when this timeline is + /** The index of the physics constraint in {@link Skeleton.contraints} that will be reset when this timeline is * applied, or -1 if all physics constraints in the skeleton will be reset. */ - constraintIndex: number; + readonly constraintIndex: number; - /** @param physicsConstraintIndex -1 for all physics constraints in the skeleton. */ - constructor (frameCount: number, physicsConstraintIndex: number) { - super(frameCount, PhysicsConstraintResetTimeline.propertyIds); - this.constraintIndex = physicsConstraintIndex; + /** @param constraintIndex -1 for all physics constraints in the skeleton. */ + constructor (frameCount: number, constraintIndex: number) { + super(frameCount, ...PhysicsConstraintResetTimeline.propertyIds); + this.constraintIndex = constraintIndex; } getFrameCount () { @@ -2243,132 +2402,66 @@ export class PhysicsConstraintResetTimeline extends Timeline { } /** Resets the physics constraint when frames > lastTime and <= time. */ - apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, direction: MixDirection) { + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { let constraint: PhysicsConstraint | undefined; - if (this.constraintIndex != -1) { - constraint = skeleton.physicsConstraints[this.constraintIndex]; + if (this.constraintIndex !== -1) { + constraint = skeleton.constraints[this.constraintIndex] as PhysicsConstraint; if (!constraint.active) return; } const frames = this.frames; if (lastTime > time) { // Apply after lastTime for looped animations. - this.apply(skeleton, lastTime, Number.MAX_VALUE, [], alpha, blend, direction); + this.apply(skeleton, lastTime, Number.MAX_VALUE, [], alpha, blend, direction, appliedPose); lastTime = -1; } else if (lastTime >= frames[frames.length - 1]) // Last time is after last frame. return; if (time < frames[0]) return; - if (lastTime < frames[0] || time >= frames[Timeline.search1(frames, lastTime) + 1]) { + if (lastTime < frames[0] || time >= frames[Timeline.search(frames, lastTime) + 1]) { if (constraint != null) - constraint.reset(); + constraint.reset(skeleton); else { - for (const constraint of skeleton.physicsConstraints) { - if (constraint.active) constraint.reset(); + for (const constraint of skeleton.physics) { + if (constraint.active) constraint.reset(skeleton); } } } } } -/** Changes a slot's {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */ -export class SequenceTimeline extends Timeline implements SlotTimeline { - static ENTRIES = 3; - static MODE = 1; - static DELAY = 2; - - slotIndex: number; - attachment: HasTextureRegion; - - constructor (frameCount: number, slotIndex: number, attachment: HasTextureRegion) { - super(frameCount, [ - Property.sequence + "|" + slotIndex + "|" + attachment.sequence!.id - ]); - this.slotIndex = slotIndex; - this.attachment = attachment; +/** Changes a slider's {@link SliderPose.time()}. */ +export class SliderTimeline extends ConstraintTimeline1 { + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.sliderTime); } - getFrameEntries () { - return SequenceTimeline.ENTRIES; - } + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { - getSlotIndex () { - return this.slotIndex; - } - - getAttachment () { - return this.attachment as unknown as Attachment; - } - - /** Sets the time, mode, index, and frame time for the specified frame. - * @param frame Between 0 and frameCount, inclusive. - * @param time Seconds between frames. */ - setFrame (frame: number, time: number, mode: SequenceMode, index: number, delay: number) { - let frames = this.frames; - frame *= SequenceTimeline.ENTRIES; - frames[frame] = time; - frames[frame + SequenceTimeline.MODE] = mode | (index << 4); - frames[frame + SequenceTimeline.DELAY] = delay; - } - - apply (skeleton: Skeleton, lastTime: number, time: number, events: Array, alpha: number, blend: MixBlend, direction: MixDirection) { - let slot = skeleton.slots[this.slotIndex]; - if (!slot.bone.active) return; - let slotAttachment = slot.attachment; - let attachment = this.attachment as unknown as Attachment; - if (slotAttachment != attachment) { - if (!(slotAttachment instanceof VertexAttachment) - || slotAttachment.timelineAttachment != attachment) return; + const constraint = skeleton.constraints[this.constraintIndex]; + if (constraint.active) { + const pose = appliedPose ? constraint.applied : constraint.pose; + pose.time = this.getAbsoluteValue(time, alpha, blend, pose.time, constraint.data.setup.time); + } + } +} + +/** Changes a slider's {@link SliderPose.mix()}. */ +export class SliderMixTimeline extends ConstraintTimeline1 { + constructor (frameCount: number, bezierCount: number, constraintIndex: number) { + super(frameCount, bezierCount, constraintIndex, Property.sliderMix); + } + + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend, + direction: MixDirection, appliedPose: boolean) { + + const constraint = skeleton.constraints[this.constraintIndex]; + if (constraint.active) { + const pose = appliedPose ? constraint.applied : constraint.pose; + pose.mix = this.getAbsoluteValue(time, alpha, blend, pose.mix, constraint.data.setup.mix); } - - if (direction == MixDirection.mixOut) { - if (blend == MixBlend.setup) slot.sequenceIndex = -1; - return; - } - - let frames = this.frames; - if (time < frames[0]) { - if (blend == MixBlend.setup || blend == MixBlend.first) slot.sequenceIndex = -1; - return; - } - - let i = Timeline.search(frames, time, SequenceTimeline.ENTRIES); - let before = frames[i]; - let modeAndIndex = frames[i + SequenceTimeline.MODE]; - let delay = frames[i + SequenceTimeline.DELAY]; - - if (!this.attachment.sequence) return; - let index = modeAndIndex >> 4, count = this.attachment.sequence!.regions.length; - let mode = SequenceModeValues[modeAndIndex & 0xf]; - if (mode != SequenceMode.hold) { - index += (((time - before) / delay + 0.00001) | 0); - switch (mode) { - case SequenceMode.once: - index = Math.min(count - 1, index); - break; - case SequenceMode.loop: - index %= count; - break; - case SequenceMode.pingpong: { - let n = (count << 1) - 2; - index = n == 0 ? 0 : index % n; - if (index >= count) index = n - index; - break; - } - case SequenceMode.onceReverse: - index = Math.max(count - 1 - index, 0); - break; - case SequenceMode.loopReverse: - index = count - 1 - (index % count); - break; - case SequenceMode.pingpongReverse: { - let n = (count << 1) - 2; - index = n == 0 ? 0 : (index + count - 1) % n; - if (index >= count) index = n - index; - } - } - } - slot.sequenceIndex = index; } } diff --git a/spine-ts/spine-core/src/AnimationState.ts b/spine-ts/spine-core/src/AnimationState.ts index df29e1251..a3a587ea0 100644 --- a/spine-ts/spine-core/src/AnimationState.ts +++ b/spine-ts/spine-core/src/AnimationState.ts @@ -40,16 +40,13 @@ import { Event } from "./Event.js"; * * See [Applying Animations](http://esotericsoftware.com/spine-applying-animations/) in the Spine Runtimes Guide. */ export class AnimationState { - static _emptyAnimation = new Animation("", [], 0); - private static emptyAnimation (): Animation { - return AnimationState._emptyAnimation; - } + 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. */ - tracks = new Array(); + readonly tracks = new Array(); /** 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. @@ -58,8 +55,8 @@ export class AnimationState { timeScale = 1; unkeyedState = 0; - events = new Array(); - listeners = new Array(); + readonly events = new Array(); + readonly listeners = new Array(); queue = new EventQueue(this); propertyIDs = new StringSet(); animationsChanged = false; @@ -197,11 +194,11 @@ export class AnimationState { // 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); - var timeline = timelines[ii]; + const timeline = timelines[ii]; if (timeline instanceof AttachmentTimeline) this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments); else - timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.mixIn); + timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false); } } else { let timelineMode = current.timelineMode; @@ -220,7 +217,7 @@ export class AnimationState { } 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.mixIn); + timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.in, false); } } } @@ -233,13 +230,13 @@ export class AnimationState { // Set slots attachments to the setup pose, if needed. This occurs if an animation that is mixing out sets attachments so // subsequent timelines see any deform, but the subsequent timelines don't set an attachment (eg they are also mixing out or // the time is before the first key). - var setupState = this.unkeyedState + SETUP; - var slots = skeleton.slots; - for (var i = 0, n = skeleton.slots.length; i < n; i++) { - var slot = slots[i]; + const setupState = this.unkeyedState + SETUP; + const slots = skeleton.slots; + for (let i = 0, n = skeleton.slots.length; i < n; i++) { + const slot = slots[i]; if (slot.attachmentState == setupState) { - var attachmentName = slot.data.attachmentName; - slot.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName)); + const attachmentName = slot.data.attachmentName; + slot.pose.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName)); } } this.unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot. @@ -275,7 +272,7 @@ export class AnimationState { if (blend == MixBlend.add) { for (let i = 0; i < timelineCount; i++) - timelines[i].apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.mixOut); + timelines[i].apply(skeleton, animationLast, applyTime, events, alphaMix, blend, MixDirection.out, false); } else { let timelineMode = from.timelineMode; let timelineHoldMix = from.timelineHoldMix; @@ -287,7 +284,7 @@ export class AnimationState { from.totalAlpha = 0; for (let i = 0; i < timelineCount; i++) { let timeline = timelines[i]; - let direction = MixDirection.mixOut; + let direction = MixDirection.out; let timelineBlend: MixBlend; let alpha = 0; switch (timelineMode[i]) { @@ -308,7 +305,7 @@ export class AnimationState { timelineBlend = MixBlend.setup; alpha = alphaHold; break; - default: + default: // HOLD_MIX timelineBlend = MixBlend.setup; let holdMix = timelineHoldMix[i]; alpha = alphaHold * Math.max(0, 1 - holdMix.mixTime / holdMix.mixDuration); @@ -324,8 +321,8 @@ export class AnimationState { // 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.mixIn; - timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction); + direction = MixDirection.in; + timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false); } } } @@ -339,21 +336,21 @@ export class AnimationState { } applyAttachmentTimeline (timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, attachments: boolean) { - var slot = skeleton.slots[timeline.slotIndex]; + const slot = skeleton.slots[timeline.slotIndex]; if (!slot.bone.active) return; 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); } else - this.setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search1(timeline.frames, time)], attachments); + this.setAttachment(skeleton, slot, timeline.attachmentNames[Timeline.search(timeline.frames, time)], attachments); // If an attachment wasn't set (ie before the first frame or attachments is false), set the setup attachment later. if (slot.attachmentState <= this.unkeyedState) slot.attachmentState = this.unkeyedState + SETUP; } setAttachment (skeleton: Skeleton, slot: Slot, attachmentName: string | null, attachments: boolean) { - slot.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName)); + slot.pose.setAttachment(!attachmentName ? null : skeleton.getAttachment(slot.data.index, attachmentName)); if (attachments) slot.attachmentState = this.unkeyedState + CURRENT; } @@ -363,27 +360,28 @@ export class AnimationState { if (firstFrame) timelinesRotation[i] = 0; if (alpha == 1) { - timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn); + timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.in, false); return; } let bone = skeleton.bones[timeline.boneIndex]; if (!bone.active) return; + const pose = bone.pose, setup = bone.data.setup; let frames = timeline.frames; let r1 = 0, r2 = 0; if (time < frames[0]) { switch (blend) { case MixBlend.setup: - bone.rotation = bone.data.rotation; + pose.rotation = setup.rotation; default: return; case MixBlend.first: - r1 = bone.rotation; - r2 = bone.data.rotation; + r1 = pose.rotation; + r2 = setup.rotation; } } else { - r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation; - r2 = bone.data.rotation + timeline.getCurveValue(time); + r1 = blend == MixBlend.setup ? setup.rotation : pose.rotation; + r2 = setup.rotation + timeline.getCurveValue(time); } // Mix between rotations using the direction of the shortest route on the first frame while detecting crosses. @@ -416,7 +414,7 @@ export class AnimationState { timelinesRotation[i] = total; } timelinesRotation[i + 1] = diff; - bone.rotation = r1 + total * alpha; + pose.rotation = r1 + total * alpha; } queueEvents (entry: TrackEntry, animationTime: number) { @@ -519,26 +517,47 @@ export class AnimationState { } /** Sets an animation by name. - * - * See {@link #setAnimationWith()}. */ - setAnimation (trackIndex: number, animationName: string, loop: boolean = false) { - let animation = this.data.skeletonData.findAnimation(animationName); - if (!animation) throw new Error("Animation not found: " + animationName); - return this.setAnimationWith(trackIndex, animation, loop); + * + * See {@link setAnimation}. */ + setAnimation (trackIndex: number, animationName: string, loop?: boolean): TrackEntry; + + /** Sets the current animation for a track, discarding any queued animations. + * + * If the formerly current track entry 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 + * duration. In either case {@link TrackEntry.trackEnd} determines when the track is cleared. + * @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. */ + setAnimation (trackIndex: number, animation: Animation, loop?: boolean): TrackEntry; + + setAnimation (trackIndex: number, animationNameOrAnimation: string | Animation, loop = false): TrackEntry { + if (typeof animationNameOrAnimation === "string") + return this.setAnimation1(trackIndex, animationNameOrAnimation, loop); + return this.setAnimation2(trackIndex, animationNameOrAnimation, loop); } - /** Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never - * applied to a skeleton, it is replaced (not mixed from). + private setAnimation1 (trackIndex: number, animationName: string, loop: boolean = false) { + let animation = this.data.skeletonData.findAnimation(animationName); + if (!animation) throw new Error("Animation not found: " + animationName); + return this.setAnimation2(trackIndex, animation, loop); + } + + /** 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 - * duration. In either case {@link TrackEntry#trackEnd} determines when the track is cleared. - * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept - * after the {@link AnimationStateListener#dispose()} event occurs. */ - setAnimationWith (trackIndex: number, animation: Animation, loop: boolean = false) { + * duration. In either case {@link TrackEntry#getTrackEnd()} determines when the track is cleared. + * @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. */ + private setAnimation2 (trackIndex: number, animation: Animation, loop: boolean = false) { + if (trackIndex < 0) throw new Error("trackIndex must be >= 0."); if (!animation) throw new Error("animation cannot be null."); let interrupt = true; let current = this.expandToIndex(trackIndex); if (current) { - if (current.nextTrackLast == -1) { + if (current.nextTrackLast === -1 && current.animation === animation) { // Don't mix from an entry that was never applied. this.tracks[trackIndex] = current.mixingFrom; this.queue.interrupt(current); @@ -557,22 +576,32 @@ export class AnimationState { /** Queues an animation by name. * - * See {@link #addAnimationWith()}. */ - addAnimation (trackIndex: number, animationName: string, loop: boolean = false, delay: number = 0) { - let animation = this.data.skeletonData.findAnimation(animationName); - if (!animation) throw new Error("Animation not found: " + animationName); - return this.addAnimationWith(trackIndex, animation, loop, delay); - } + * See {@link addAnimation}. */ + addAnimation (trackIndex: number, animationName: string, loop?: boolean, delay?: number): TrackEntry; /** Adds an animation to be played after the current or last queued animation for a track. If the track has no entries, this is - * equivalent to calling {@link #setAnimationWith()}. - * @param delay If > 0, sets {@link TrackEntry#delay}. If <= 0, the delay set is the duration of the previous track entry - * minus any mix duration (from the {@link AnimationStateData}) plus the specified `delay` (ie the mix - * ends at (`delay` = 0) or before (`delay` < 0) the previous track entry duration). If the + * 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 * previous entry is looping, its next loop completion is used instead of its duration. - * @returns A track entry to allow further customization of animation playback. References to the track entry must not be kept - * after the {@link AnimationStateListener#dispose()} event occurs. */ - addAnimationWith (trackIndex: number, animation: Animation, loop: boolean = false, delay: number = 0) { + * @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. */ + addAnimation (trackIndex: number, animation: Animation, loop?: boolean, delay?: number): TrackEntry; + + addAnimation (trackIndex: number, animationNameOrAnimation: string | Animation, loop = false, delay: number = 0): TrackEntry { + if (typeof animationNameOrAnimation === "string") + return this.addAnimation1(trackIndex, animationNameOrAnimation, loop, delay); + return this.addAnimation2(trackIndex, animationNameOrAnimation, loop, delay); + } + + private addAnimation1 (trackIndex: number, animationName: string, loop: boolean = false, delay: number = 0) { + let animation = this.data.skeletonData.findAnimation(animationName); + if (!animation) throw new Error("Animation not found: " + animationName); + return this.addAnimation2(trackIndex, animation, loop, delay); + } + + private addAnimation2 (trackIndex: number, animation: Animation, loop: boolean = false, delay: number = 0) { if (!animation) throw new Error("animation cannot be null."); let last = this.expandToIndex(trackIndex); @@ -611,11 +640,11 @@ export class AnimationState { * {@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. - *

+ * * See Empty animations in the Spine * Runtimes Guide. */ setEmptyAnimation (trackIndex: number, mixDuration: number = 0) { - let entry = this.setAnimationWith(trackIndex, AnimationState.emptyAnimation(), false); + let entry = this.setAnimation(trackIndex, AnimationState.emptyAnimation, false); entry.mixDuration = mixDuration; entry.trackEnd = mixDuration; return entry; @@ -624,7 +653,7 @@ 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 #setEmptyAnimation(int, float)}. - *

+ * * See {@link #setEmptyAnimation(int, float)} and * Empty animations in the Spine * Runtimes Guide. @@ -635,7 +664,7 @@ export class AnimationState { * @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) { - let entry = this.addAnimationWith(trackIndex, AnimationState.emptyAnimation(), false, delay); + let entry = this.addAnimation(trackIndex, AnimationState.emptyAnimation, false, delay); if (delay <= 0) entry.delay = Math.max(entry.delay + entry.mixDuration - mixDuration, 0); entry.mixDuration = mixDuration; entry.trackEnd = mixDuration; @@ -643,7 +672,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 * Runtimes Guide. */ setEmptyAnimations (mixDuration: number = 0) { @@ -951,18 +980,17 @@ export class TrackEntry { * 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; interruptAlpha: number = 0; totalAlpha: number = 0; + mixDuration: number = 0; - get mixDuration () { - return this._mixDuration; - } + interruptAlpha: number = 0; totalAlpha: number = 0; - set mixDuration (mixDuration: number) { - this._mixDuration = mixDuration; - } - - setMixDurationWithDelay (mixDuration: number, delay: number) { - this._mixDuration = mixDuration; + /** 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. */ + setMixDuration (mixDuration: number, delay: number) { + this.mixDuration = mixDuration; if (delay <= 0) { if (this.previous != null) delay = Math.max(delay + this.previous.getTrackComplete() - mixDuration, 0); @@ -1096,41 +1124,41 @@ export class EventQueue { } drain () { - if (this.drainDisabled) return; + if (this.drainDisabled) return; // Not reentrant. this.drainDisabled = true; - let objects = this.objects; - let listeners = this.animState.listeners; + const listeners = this.animState.listeners; + const objects = this.objects; for (let i = 0; i < objects.length; i += 2) { - let type = objects[i] as EventType; - let entry = objects[i + 1] as TrackEntry; + const type = objects[i] as EventType; + const entry = objects[i + 1] as TrackEntry; switch (type) { case EventType.start: if (entry.listener && entry.listener.start) entry.listener.start(entry); for (let ii = 0; ii < listeners.length; ii++) { - let listener = listeners[ii]; + const listener = listeners[ii]; if (listener.start) listener.start(entry); } break; case EventType.interrupt: if (entry.listener && entry.listener.interrupt) entry.listener.interrupt(entry); for (let ii = 0; ii < listeners.length; ii++) { - let listener = listeners[ii]; + const listener = listeners[ii]; if (listener.interrupt) listener.interrupt(entry); } break; case EventType.end: if (entry.listener && entry.listener.end) entry.listener.end(entry); for (let ii = 0; ii < listeners.length; ii++) { - let listener = listeners[ii]; + const listener = listeners[ii]; if (listener.end) listener.end(entry); } // Fall through. case EventType.dispose: if (entry.listener && entry.listener.dispose) entry.listener.dispose(entry); for (let ii = 0; ii < listeners.length; ii++) { - let listener = listeners[ii]; + const listener = listeners[ii]; if (listener.dispose) listener.dispose(entry); } this.animState.trackEntryPool.free(entry); @@ -1138,15 +1166,15 @@ export class EventQueue { case EventType.complete: if (entry.listener && entry.listener.complete) entry.listener.complete(entry); for (let ii = 0; ii < listeners.length; ii++) { - let listener = listeners[ii]; + const listener = listeners[ii]; if (listener.complete) listener.complete(entry); } break; case EventType.event: - let event = objects[i++ + 2] as Event; + const event = objects[i++ + 2] as Event; if (entry.listener && entry.listener.event) entry.listener.event(entry, event); for (let ii = 0; ii < listeners.length; ii++) { - let listener = listeners[ii]; + const listener = listeners[ii]; if (listener.event) listener.event(entry, event); } break; @@ -1175,11 +1203,11 @@ export enum EventType { * See TrackEntry {@link TrackEntry#listener} and AnimationState * {@link AnimationState#addListener}. */ export interface AnimationStateListener { - /** Invoked when this entry has been set as the current entry. {@link #end(TrackEntry)} will occur when this entry will no + /** Invoked when this entry has been set as the current entry. {@link end} will occur when this entry will no * longer be applied. - *

- * When this event is triggered by calling {@link AnimationState#setAnimation}, take care not to - * call {@link AnimationState#update} until after the TrackEntry has been configured. */ + * + * When this event is triggered by calling {@link AnimationState.setAnimation}, take care not to + * call {@link AnimationState.update} until after the TrackEntry has been configured. */ start?: (entry: TrackEntry) => void; /** Invoked when another entry has replaced this entry as the current entry. This entry may continue being applied for diff --git a/spine-ts/spine-core/src/Bone.ts b/spine-ts/spine-core/src/Bone.ts index 3c71bd20b..2113b29f1 100644 --- a/spine-ts/spine-core/src/Bone.ts +++ b/spine-ts/spine-core/src/Bone.ts @@ -27,413 +27,37 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { BoneData, Inherit } from "./BoneData.js"; -import { Physics, Skeleton } from "./Skeleton.js"; -import { Updatable } from "./Updatable.js"; -import { MathUtils, Vector2 } from "./Utils.js"; +import { BoneData } from "./BoneData.js"; +import { BoneLocal } from "./BoneLocal.js"; +import { BonePose } from "./BonePose.js"; +import { PosedActive } from "./PosedActive.js"; -/** Stores a bone's current pose. +/** 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 implements Updatable { - /** The bone's setup pose data. */ - data: BoneData; - - /** The skeleton this bone belongs to. */ - skeleton: Skeleton; - +export class Bone extends PosedActive { /** The parent bone, or null if this is the root bone. */ parent: Bone | null = null; /** The immediate children of this bone. */ children = new Array(); - /** 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; - - /** The applied local x translation. */ - ax = 0; - - /** The applied local y translation. */ - ay = 0; - - /** The applied local rotation in degrees, counter clockwise. */ - arotation = 0; - - /** The applied local scaleX. */ - ascaleX = 0; - - /** The applied local scaleY. */ - ascaleY = 0; - - /** The applied local shearX. */ - ashearX = 0; - - /** The applied local shearY. */ - ashearY = 0; - - /** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */ - a = 0; - - /** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */ - b = 0; - - /** Part of the world transform matrix for the X axis. If changed, {@link #updateAppliedTransform()} should be called. */ - c = 0; - - /** Part of the world transform matrix for the Y axis. If changed, {@link #updateAppliedTransform()} should be called. */ - d = 0; - - /** The world X position. If changed, {@link #updateAppliedTransform()} should be called. */ - worldY = 0; - - /** The world Y position. If changed, {@link #updateAppliedTransform()} should be called. */ - worldX = 0; - - inherit: Inherit = Inherit.Normal; - sorted = false; - active = false; - /** @param parent May be null. */ - constructor (data: BoneData, skeleton: Skeleton, parent: Bone | null) { - if (!data) throw new Error("data cannot be null."); - if (!skeleton) throw new Error("skeleton cannot be null."); - this.data = data; - this.skeleton = skeleton; + constructor (data: BoneData, parent: Bone | null) { + super(data, new BonePose(), new BonePose()); this.parent = parent; - this.setToSetupPose(); + this.applied.bone = this; + this.constrained.bone = this; } - /** Returns false when the bone has not been computed because {@link BoneData#skinRequired} is true and the - * {@link Skeleton#skin active skin} does not {@link Skin#bones contain} this bone. */ - isActive () { - return this.active; + /** Make a copy of the bone. Does not copy the {@link #getChildren()} bones. */ + copy (parent: Bone | null): Bone { + const copy = new Bone(this.data, parent); + copy.pose.set(this.pose); + return copy; } - /** Computes the world transform using the parent bone and this bone's local applied transform. */ - update (physics: Physics | null) { - this.updateWorldTransformWith(this.ax, this.ay, this.arotation, this.ascaleX, this.ascaleY, this.ashearX, this.ashearY); - } - - /** Computes the world transform using the parent bone and this bone's local transform. - * - * See {@link #updateWorldTransformWith()}. */ - updateWorldTransform () { - this.updateWorldTransformWith(this.x, this.y, this.rotation, this.scaleX, this.scaleY, this.shearX, this.shearY); - } - - /** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the - * specified local transform. Child bones are not updated. - * - * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine - * Runtimes Guide. */ - updateWorldTransformWith (x: number, y: number, rotation: number, scaleX: number, scaleY: number, shearX: number, shearY: number) { - this.ax = x; - this.ay = y; - this.arotation = rotation; - this.ascaleX = scaleX; - this.ascaleY = scaleY; - this.ashearX = shearX; - this.ashearY = shearY; - - let parent = this.parent; - if (!parent) { // Root bone. - let skeleton = this.skeleton; - const sx = skeleton.scaleX, sy = skeleton.scaleY; - const rx = (rotation + shearX) * MathUtils.degRad; - const ry = (rotation + 90 + shearY) * MathUtils.degRad; - this.a = Math.cos(rx) * scaleX * sx; - this.b = Math.cos(ry) * scaleY * sx; - this.c = Math.sin(rx) * scaleX * sy; - this.d = Math.sin(ry) * scaleY * sy; - this.worldX = x * sx + skeleton.x; - this.worldY = y * sy + skeleton.y; - return; - } - - let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; - this.worldX = pa * x + pb * y + parent.worldX; - this.worldY = pc * x + pd * y + parent.worldY; - - switch (this.inherit) { - case Inherit.Normal: { - const rx = (rotation + shearX) * MathUtils.degRad; - const ry = (rotation + 90 + shearY) * MathUtils.degRad; - const la = Math.cos(rx) * scaleX; - const lb = Math.cos(ry) * scaleY; - const lc = Math.sin(rx) * scaleX; - const ld = Math.sin(ry) * scaleY; - this.a = pa * la + pb * lc; - this.b = pa * lb + pb * ld; - this.c = pc * la + pd * lc; - this.d = pc * lb + pd * ld; - return; - } - case Inherit.OnlyTranslation: { - const rx = (rotation + shearX) * MathUtils.degRad; - const ry = (rotation + 90 + shearY) * MathUtils.degRad; - this.a = Math.cos(rx) * scaleX; - this.b = Math.cos(ry) * scaleY; - this.c = Math.sin(rx) * scaleX; - this.d = Math.sin(ry) * scaleY; - break; - } - case Inherit.NoRotationOrReflection: { - let sx = 1 / this.skeleton.scaleX, sy = 1 / this.skeleton.scaleY; - pa *= sx; - pc *= sy; - let s = pa * pa + pc * pc; - let prx = 0; - if (s > 0.0001) { - s = Math.abs(pa * pd * sy - pb * sx * pc) / s; - pb = pc * s; - pd = pa * s; - prx = Math.atan2(pc, pa) * MathUtils.radDeg; - } else { - pa = 0; - pc = 0; - prx = 90 - Math.atan2(pd, pb) * MathUtils.radDeg; - } - const rx = (rotation + shearX - prx) * MathUtils.degRad; - const ry = (rotation + shearY - prx + 90) * MathUtils.degRad; - const la = Math.cos(rx) * scaleX; - const lb = Math.cos(ry) * scaleY; - const lc = Math.sin(rx) * scaleX; - const ld = Math.sin(ry) * scaleY; - this.a = pa * la - pb * lc; - this.b = pa * lb - pb * ld; - this.c = pc * la + pd * lc; - this.d = pc * lb + pd * ld; - break; - } - case Inherit.NoScale: - case Inherit.NoScaleOrReflection: { - rotation *= MathUtils.degRad; - const cos = Math.cos(rotation), sin = Math.sin(rotation); - let za = (pa * cos + pb * sin) / this.skeleton.scaleX; - let zc = (pc * cos + pd * sin) / this.skeleton.scaleY; - let s = Math.sqrt(za * za + zc * zc); - if (s > 0.00001) s = 1 / s; - za *= s; - zc *= s; - s = Math.sqrt(za * za + zc * zc); - if (this.inherit == Inherit.NoScale - && (pa * pd - pb * pc < 0) != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0)) s = -s; - rotation = Math.PI / 2 + Math.atan2(zc, za); - const zb = Math.cos(rotation) * s; - const zd = Math.sin(rotation) * s; - shearX *= MathUtils.degRad; - shearY = (90 + shearY) * MathUtils.degRad; - const la = Math.cos(shearX) * scaleX; - const lb = Math.cos(shearY) * scaleY; - const lc = Math.sin(shearX) * scaleX; - const ld = Math.sin(shearY) * scaleY; - this.a = za * la + zb * lc; - this.b = za * lb + zb * ld; - this.c = zc * la + zd * lc; - this.d = zc * lb + zd * ld; - break; - } - } - this.a *= this.skeleton.scaleX; - this.b *= this.skeleton.scaleX; - this.c *= this.skeleton.scaleY; - this.d *= this.skeleton.scaleY; - } - - /** Sets this bone's local transform to the setup pose. */ - setToSetupPose () { - let data = this.data; - this.x = data.x; - this.y = data.y; - this.rotation = data.rotation; - this.scaleX = data.scaleX; - this.scaleY = data.scaleY; - this.shearX = data.shearX; - this.shearY = data.shearY; - this.inherit = data.inherit; - } - - /** Computes the applied transform values from the world transform. - * - * If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so - * the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply other - * constraints). - * - * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after - * calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */ - updateAppliedTransform () { - let parent = this.parent; - if (!parent) { - this.ax = this.worldX - this.skeleton.x; - this.ay = this.worldY - this.skeleton.y; - this.arotation = Math.atan2(this.c, this.a) * MathUtils.radDeg; - this.ascaleX = Math.sqrt(this.a * this.a + this.c * this.c); - this.ascaleY = Math.sqrt(this.b * this.b + this.d * this.d); - this.ashearX = 0; - this.ashearY = Math.atan2(this.a * this.b + this.c * this.d, this.a * this.d - this.b * this.c) * MathUtils.radDeg; - return; - } - 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; - let dx = this.worldX - parent.worldX, dy = this.worldY - parent.worldY; - this.ax = (dx * ia - dy * ib); - this.ay = (dy * id - dx * ic); - - let ra, rb, rc, rd; - if (this.inherit == Inherit.OnlyTranslation) { - ra = this.a; - rb = this.b; - rc = this.c; - rd = this.d; - } else { - switch (this.inherit) { - case Inherit.NoRotationOrReflection: { - let s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc); - pb = -pc * this.skeleton.scaleX * s / this.skeleton.scaleY; - pd = pa * this.skeleton.scaleY * s / this.skeleton.scaleX; - pid = 1 / (pa * pd - pb * pc); - ia = pd * pid; - ib = pb * pid; - break; - } - case Inherit.NoScale: - case Inherit.NoScaleOrReflection: - let cos = MathUtils.cosDeg(this.rotation), sin = MathUtils.sinDeg(this.rotation); - pa = (pa * cos + pb * sin) / this.skeleton.scaleX; - pc = (pc * cos + pd * sin) / this.skeleton.scaleY; - let s = Math.sqrt(pa * pa + pc * pc); - if (s > 0.00001) s = 1 / s; - pa *= s; - pc *= s; - s = Math.sqrt(pa * pa + pc * pc); - if (this.inherit == Inherit.NoScale && pid < 0 != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0)) s = -s; - let r = MathUtils.PI / 2 + Math.atan2(pc, pa); - pb = Math.cos(r) * s; - pd = Math.sin(r) * s; - pid = 1 / (pa * pd - pb * pc); - ia = pd * pid; - ib = pb * pid; - ic = pc * pid; - id = pa * pid; - } - ra = ia * this.a - ib * this.c; - rb = ia * this.b - ib * this.d; - rc = id * this.c - ic * this.a; - rd = id * this.d - ic * this.b; - } - - this.ashearX = 0; - this.ascaleX = Math.sqrt(ra * ra + rc * rc); - if (this.ascaleX > 0.0001) { - let det = ra * rd - rb * rc; - this.ascaleY = det / this.ascaleX; - this.ashearY = -Math.atan2(ra * rb + rc * rd, det) * MathUtils.radDeg; - this.arotation = Math.atan2(rc, ra) * MathUtils.radDeg; - } else { - this.ascaleX = 0; - this.ascaleY = Math.sqrt(rb * rb + rd * rd); - this.ashearY = 0; - this.arotation = 90 - Math.atan2(rd, rb) * MathUtils.radDeg; - } - } - - - /** The world rotation for the X axis, calculated using {@link #a} and {@link #c}. */ - getWorldRotationX () { - return Math.atan2(this.c, this.a) * MathUtils.radDeg; - } - - /** The world rotation for the Y axis, calculated using {@link #b} and {@link #d}. */ - getWorldRotationY () { - return Math.atan2(this.d, this.b) * MathUtils.radDeg; - } - - /** The magnitude (always positive) of the world scale X, calculated using {@link #a} and {@link #c}. */ - getWorldScaleX () { - return Math.sqrt(this.a * this.a + this.c * this.c); - } - - /** The magnitude (always positive) of the world scale Y, calculated using {@link #b} and {@link #d}. */ - getWorldScaleY () { - return Math.sqrt(this.b * this.b + this.d * this.d); - } - - /** Transforms a point from world coordinates to the bone's local coordinates. */ - worldToLocal (world: Vector2) { - let invDet = 1 / (this.a * this.d - this.b * this.c); - let x = world.x - this.worldX, y = world.y - this.worldY; - world.x = x * this.d * invDet - y * this.b * invDet; - world.y = y * this.a * invDet - x * this.c * invDet; - return world; - } - - /** Transforms a point from the bone's local coordinates to world coordinates. */ - localToWorld (local: Vector2) { - let x = local.x, y = local.y; - local.x = x * this.a + y * this.b + this.worldX; - local.y = x * this.c + y * this.d + this.worldY; - return local; - } - - /** Transforms a point from world coordinates to the parent bone's local coordinates. */ - worldToParent (world: Vector2) { - if (world == null) throw new Error("world cannot be null."); - return this.parent == null ? world : this.parent.worldToLocal(world); - } - - /** Transforms a point from the parent bone's coordinates to world coordinates. */ - parentToWorld (world: Vector2) { - if (world == null) throw new Error("world cannot be null."); - return this.parent == null ? world : this.parent.localToWorld(world); - } - - /** Transforms a world rotation to a local rotation. */ - worldToLocalRotation (worldRotation: number) { - let sin = MathUtils.sinDeg(worldRotation), cos = MathUtils.cosDeg(worldRotation); - return Math.atan2(this.a * sin - this.c * cos, this.d * cos - this.b * sin) * MathUtils.radDeg + this.rotation - this.shearX; - } - - /** Transforms a local rotation to a world rotation. */ - localToWorldRotation (localRotation: number) { - localRotation -= this.rotation - this.shearX; - let sin = MathUtils.sinDeg(localRotation), cos = MathUtils.cosDeg(localRotation); - return Math.atan2(cos * this.c + sin * this.d, cos * this.a + sin * this.b) * MathUtils.radDeg; - } - - /** Rotates the world transform the specified amount. - *

- * After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and - * {@link #update(Physics)} will need to be called on any child bones, recursively. */ - rotateWorld (degrees: number) { - degrees *= MathUtils.degRad; - const sin = Math.sin(degrees), cos = Math.cos(degrees); - const ra = this.a, rb = this.b; - this.a = cos * ra - sin * this.c; - this.b = cos * rb - sin * this.d; - this.c = sin * ra + cos * this.c; - this.d = sin * rb + cos * this.d; - } } diff --git a/spine-ts/spine-core/src/BoneData.ts b/spine-ts/spine-core/src/BoneData.ts index c5499b94d..157654b7a 100644 --- a/spine-ts/spine-core/src/BoneData.ts +++ b/spine-ts/spine-core/src/BoneData.ts @@ -27,15 +27,16 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ +import { BoneLocal } from "./BoneLocal.js"; +import { PosedData } from "./PosedData.js"; import { Color } from "./Utils.js"; -/** Stores the setup pose for a {@link Bone}. */ -export class BoneData { - /** The index of the bone in {@link Skeleton#getBones()}. */ - index: number = 0; +import type { Skeleton } from "./Skeleton.js"; - /** The name of the bone, which is unique across all bones in the skeleton. */ - name: string; +/** The setup pose for a bone. */ +export class BoneData extends PosedData { + /** The index of the bone in {@link Skeleton.getBones}. */ + index: number = 0; /** @returns May be null. */ parent: BoneData | null = null; @@ -43,38 +44,10 @@ export class BoneData { /** The bone's length. */ length: number = 0; - /** 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 = 1; - - /** The local scaleY. */ - scaleY = 1; - - /** The local shearX. */ - shearX = 0; - - /** The local shearX. */ - shearY = 0; - - /** The transform mode for how parent world transforms affect this bone. */ - inherit = Inherit.Normal; - - /** When true, {@link Skeleton#updateWorldTransform()} only updates this bone if the {@link Skeleton#skin} contains this - * bone. - * @see Skin#bones */ - skinRequired = false; - + // Nonessential. /** The color of the bone as it was in Spine. Available only when nonessential data was exported. Bones are not usually * rendered at runtime. */ - color = new Color(); + readonly color = new Color(); /** The bone icon as it was in Spine, or null if nonessential data was not exported. */ icon?: string; @@ -83,12 +56,19 @@ export class BoneData { visible = false; constructor (index: number, name: string, parent: BoneData | null) { + super(name, new BoneLocal()); if (index < 0) throw new Error("index must be >= 0."); if (!name) throw new Error("name cannot be null."); this.index = index; - this.name = name; this.parent = parent; } + + copy (parent: BoneData | null): BoneData { + const copy = new BoneData(this.index, this.name, parent); + copy.length = this.length; + copy.setup.set(this.setup); + return copy; + } } /** Determines how a bone inherits world transforms from parent bones. */ diff --git a/spine-ts/spine-core/src/BoneLocal.ts b/spine-ts/spine-core/src/BoneLocal.ts new file mode 100644 index 000000000..e9898dd86 --- /dev/null +++ b/spine-ts/spine-core/src/BoneLocal.ts @@ -0,0 +1,92 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Inherit } from "./BoneData"; +import { Pose } from "./Pose" + +/** Stores a bone's local pose. */ +export class BoneLocal implements Pose { + + /** 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; + + 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; + } + + 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; + } +} diff --git a/spine-ts/spine-core/src/BonePose.ts b/spine-ts/spine-core/src/BonePose.ts new file mode 100644 index 000000000..eeb67b8fc --- /dev/null +++ b/spine-ts/spine-core/src/BonePose.ts @@ -0,0 +1,400 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Bone } from "./Bone"; +import { Inherit } from "./BoneData"; +import { BoneLocal } from "./BoneLocal"; +import { Physics } from "./Physics"; +import { Skeleton } from "./Skeleton"; +import { Update } from "./Update"; +import { MathUtils, Vector2 } from "./Utils"; + +/** 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 { + bone!: Bone; + + /** Part of the world transform matrix for the X axis. If changed, {@link updateLocalTransform()} should be called. */ + a = 0; + + /** Part of the world transform matrix for the Y axis. If changed, {@link updateLocalTransform()} should be called. */ + b = 0; + + /** Part of the world transform matrix for the X axis. If changed, {@link updateLocalTransform()} should be called. */ + c = 0; + + /** Part of the world transform matrix for the Y axis. If changed, {@link updateLocalTransform()} should be called. */ + d = 0; + + /** The world X position. If changed, {@link updateLocalTransform()} should be called. */ + worldY = 0; + + /** The world Y position. If changed, {@link updateLocalTransform()} should be called. */ + worldX = 0; + + world = 0; + local = 0; + + /** 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. + *

+ * See World transforms in the Spine + * Runtimes Guide. */ + updateWorldTransform (skeleton: Skeleton): void { + if (this.local == skeleton._update) + this.updateLocalTransform(skeleton); + else + this.world = skeleton._update; + + const rotation = this.rotation; + const scaleX = this.scaleX; + const scaleY = this.scaleY; + const shearX = this.shearX; + const shearY = this.shearY; + if (this.bone.parent == null) { // Root bone. + const sx = skeleton.scaleX, sy = skeleton.scaleY; + const rx = (rotation + shearX) * MathUtils.degRad; + const ry = (rotation + 90 + shearY) * MathUtils.degRad; + this.a = Math.cos(rx) * scaleX * sx; + this.b = Math.cos(ry) * scaleY * sx; + this.c = Math.sin(rx) * scaleX * sy; + this.d = Math.sin(ry) * scaleY * sy; + this.worldX = this.x * sx + skeleton.x; + this.worldY = this.y * sy + skeleton.y; + return; + } + + const parent = this.bone.parent.applied; + 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; + + switch (this.inherit) { + case Inherit.Normal: { + const rx = (rotation + shearX) * MathUtils.degRad; + const ry = (rotation + 90 + shearY) * MathUtils.degRad; + const la = Math.cos(rx) * scaleX; + const lb = Math.cos(ry) * scaleY; + const lc = Math.sin(rx) * scaleX; + const ld = Math.sin(ry) * scaleY; + this.a = pa * la + pb * lc; + this.b = pa * lb + pb * ld; + this.c = pc * la + pd * lc; + this.d = pc * lb + pd * ld; + return; + } + case Inherit.OnlyTranslation: { + const rx = (rotation + shearX) * MathUtils.degRad; + const ry = (rotation + 90 + shearY) * MathUtils.degRad; + this.a = Math.cos(rx) * scaleX; + this.b = Math.cos(ry) * scaleY; + this.c = Math.sin(rx) * scaleX; + this.d = Math.sin(ry) * scaleY; + break; + } + case Inherit.NoRotationOrReflection: { + let sx = 1 / skeleton.scaleX, sy = 1 / skeleton.scaleY; + pa *= sx; + pc *= sy; + let s = pa * pa + pc * pc; + let prx = 0; + if (s > 0.0001) { + s = Math.abs(pa * pd * sy - pb * sx * pc) / s; + pb = pc * s; + pd = pa * s; + prx = MathUtils.atan2Deg(pc, pa); + } else { + pa = 0; + pc = 0; + prx = 90 - MathUtils.atan2Deg(pd, pb); + } + const rx = (rotation + shearX - prx) * MathUtils.degRad; + const ry = (rotation + shearY - prx + 90) * MathUtils.degRad; + const la = Math.cos(rx) * scaleX; + const lb = Math.cos(ry) * scaleY; + const lc = Math.sin(rx) * scaleX; + const ld = Math.sin(ry) * scaleY; + this.a = pa * la - pb * lc; + this.b = pa * lb - pb * ld; + this.c = pc * la + pd * lc; + this.d = pc * lb + pd * ld; + break; + } + case Inherit.NoScale: + case Inherit.NoScaleOrReflection: { + this.rotation *= MathUtils.degRad; + const cos = Math.cos(rotation), sin = Math.sin(rotation); + let za = (pa * cos + pb * sin) / skeleton.scaleX; + let zc = (pc * cos + pd * sin) / skeleton.scaleY; + let s = Math.sqrt(za * za + zc * zc); + if (s > 0.00001) s = 1 / s; + za *= s; + zc *= s; + s = Math.sqrt(za * za + zc * zc); + if (this.inherit == Inherit.NoScale && (pa * pd - pb * pc < 0) != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s; + this.rotation = Math.PI / 2 + Math.atan2(zc, za); + const zb = Math.cos(this.rotation) * s; + const zd = Math.sin(this.rotation) * s; + this.shearX *= MathUtils.degRad; + this.shearY = (90 + shearY) * MathUtils.degRad; + const la = Math.cos(shearX) * scaleX; + const lb = Math.cos(shearY) * scaleY; + const lc = Math.sin(shearX) * scaleX; + const ld = Math.sin(shearY) * scaleY; + this.a = za * la + zb * lc; + this.b = za * lb + zb * ld; + this.c = zc * la + zd * lc; + this.d = zc * lb + zd * ld; + break; + } + } + this.a *= skeleton.scaleX; + this.b *= skeleton.scaleX; + this.c *= skeleton.scaleY; + this.d *= skeleton.scaleY; + } + + /** Computes the local transform values from the world transform. + *

+ * If the world transform is modified (by a constraint, {@link #rotateWorld(float)}, etc) then this method should be called so + * the local transform matches the world transform. The local transform may be needed by other code (eg to apply another + * constraint). + *

+ * Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The local transform after + * calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */ + public updateLocalTransform (skeleton: Skeleton): void { + this.local = 0; + this.world = skeleton._update; + + if (!this.bone.parent) { + this.x = this.worldX - skeleton.x; + this.y = this.worldY - skeleton.y; + let a = this.a, b = this.b, c = this.c, d = this.d; + this.rotation = MathUtils.atan2Deg(c, a); + this.scaleX = Math.sqrt(a * a + c * c); + this.scaleY = Math.sqrt(b * b + d * d); + this.shearX = 0; + this.shearY = MathUtils.atan2Deg(a * b + c * d, a * d - b * c); + return; + } + + const parent = this.bone.parent.applied; + 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; + let dx = this.worldX - parent.worldX, dy = this.worldY - parent.worldY; + this.x = (dx * ia - dy * ib); + this.y = (dy * id - dx * ic); + + let ra, rb, rc, rd; + if (this.inherit == Inherit.OnlyTranslation) { + ra = this.a; + rb = this.b; + rc = this.c; + rd = this.d; + } else { + switch (this.inherit) { + case Inherit.NoRotationOrReflection: { + let s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc); + pb = -pc * skeleton.scaleX * s / skeleton.scaleY; + pd = pa * skeleton.scaleY * s / skeleton.scaleX; + pid = 1 / (pa * pd - pb * pc); + ia = pd * pid; + ib = pb * pid; + break; + } + case Inherit.NoScale: + case Inherit.NoScaleOrReflection: + let r = this.rotation * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r); + pa = (pa * cos + pb * sin) / skeleton.scaleX; + pc = (pc * cos + pd * sin) / skeleton.scaleY; + let s = Math.sqrt(pa * pa + pc * pc); + if (s > 0.00001) s = 1 / s; + pa *= s; + pc *= s; + s = Math.sqrt(pa * pa + pc * pc); + if (this.inherit == Inherit.NoScale && pid < 0 != (skeleton.scaleX < 0 != skeleton.scaleY < 0)) s = -s; + r = MathUtils.PI / 2 + Math.atan2(pc, pa); + pb = Math.cos(r) * s; + pd = Math.sin(r) * s; + pid = 1 / (pa * pd - pb * pc); + ia = pd * pid; + ib = pb * pid; + ic = pc * pid; + id = pa * pid; + } + ra = ia * this.a - ib * this.c; + rb = ia * this.b - ib * this.d; + rc = id * this.c - ic * this.a; + rd = id * this.d - ic * this.b; + } + + this.shearX = 0; + this.scaleX = Math.sqrt(ra * ra + rc * rc); + if (this.scaleX > 0.0001) { + let det = ra * rd - rb * rc; + this.scaleY = det / this.scaleX; + this.shearY = -MathUtils.atan2Deg(ra * rb + rc * rd, det); + this.rotation = MathUtils.atan2Deg(rc, ra); + } else { + this.scaleX = 0; + this.scaleY = Math.sqrt(rb * rb + rd * rd); + this.shearY = 0; + this.rotation = 90 - MathUtils.atan2Deg(rd, rb); + } + } + + /** If the world transform has been modified and the local transform no longer matches, {@link #updateLocalTransform(Skeleton)} + * is called. */ + public validateLocalTransform (skeleton: Skeleton): void { + if (this.local === skeleton._update) this.updateLocalTransform(skeleton); + } + + modifyLocal (skeleton: Skeleton): void { + if (this.local === skeleton._update) this.updateLocalTransform(skeleton); + this.world = 0; + this.resetWorld(skeleton._update); + } + + modifyWorld (update: number): void { + this.local = update; + this.world = update; + this.resetWorld(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; + if (child.world === update) { + child.world = 0; + child.local = 0; + child.resetWorld(update); + } + } + } + + /** The world rotation for the X axis, calculated using {@link a} and {@link c}. */ + public getWorldRotationX (): number { + return MathUtils.atan2Deg(this.c, this.a); + } + + /** The world rotation for the Y axis, calculated using {@link b} and {@link d}. */ + public getWorldRotationY (): number { + return MathUtils.atan2Deg(this.d, this.b); + } + + /** The magnitude (always positive) of the world scale X, calculated using {@link a} and {@link c}. */ + public getWorldScaleX (): number { + return Math.sqrt(this.a * this.a + this.c * this.c); + } + + /** The magnitude (always positive) of the world scale Y, calculated using {@link b} and {@link d}. */ + public getWorldScaleY (): number { + return Math.sqrt(this.b * this.b + this.d * this.d); + } + + // public Matrix3 getWorldTransform (Matrix3 worldTransform) { + // if (worldTransform == null) throw new IllegalArgumentException("worldTransform cannot be null."); + // float[] val = worldTransform.val; + // val[M00] = a; + // val[M01] = b; + // val[M10] = c; + // val[M11] = d; + // val[M02] = worldX; + // val[M12] = worldY; + // val[M20] = 0; + // val[M21] = 0; + // val[M22] = 1; + // return worldTransform; + // } + + /** Transforms a point from world coordinates to the bone's local coordinates. */ + public worldToLocal (world: Vector2): Vector2 { + if (world == null) throw new Error("world cannot be null."); + let det = this.a * this.d - this.b * this.c; + let x = world.x - this.worldX, y = world.y - this.worldY; + world.x = (x * this.d - y * this.b) / det; + world.y = (y * this.a - x * this.c) / det; + return world; + } + + /** Transforms a point from the bone's local coordinates to world coordinates. */ + public localToWorld (local: Vector2): Vector2 { + if (local == null) throw new Error("local cannot be null."); + let x = local.x, y = local.y; + local.x = x * this.a + y * this.b + this.worldX; + local.y = x * this.c + y * this.d + this.worldY; + return local; + } + + /** 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); + } + + /** 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); + } + + /** Transforms a world rotation to a local rotation. */ + public worldToLocalRotation (worldRotation: number): number { + worldRotation *= MathUtils.degRad; + let sin = Math.sin(worldRotation), cos = Math.cos(worldRotation); + return MathUtils.atan2Deg(this.a * sin - this.c * cos, this.d * cos - this.b * sin) + this.rotation - this.shearX; + } + + /** Transforms a local rotation to a world rotation. */ + localToWorldRotation (localRotation: number): number { + localRotation = (localRotation - this.rotation - this.shearX) * MathUtils.degRad; + let sin = Math.sin(localRotation), cos = Math.cos(localRotation); + 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. */ + rotateWorld (degrees: number) { + degrees *= MathUtils.degRad; + const sin = Math.sin(degrees), cos = Math.cos(degrees); + const ra = this.a, rb = this.b; + this.a = cos * ra - sin * this.c; + this.b = cos * rb - sin * this.d; + this.c = sin * ra + cos * this.c; + this.d = sin * rb + cos * this.d; + } +} diff --git a/spine-ts/spine-core/src/Constraint.ts b/spine-ts/spine-core/src/Constraint.ts new file mode 100644 index 000000000..78e1de526 --- /dev/null +++ b/spine-ts/spine-core/src/Constraint.ts @@ -0,0 +1,56 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { ConstraintData } from "./ConstraintData"; +import { Physics } from "./Physics"; +import { Pose } from "./Pose"; +import { PosedActive } from "./PosedActive"; +import { Skeleton } from "./Skeleton"; +import { Update } from "./Update"; + +export abstract class Constraint< + T extends Constraint, + D extends ConstraintData, + P extends Pose> + extends PosedActive implements Update { + + constructor (data: D, pose: P, constrained: P) { + super(data, pose, constrained); + } + + abstract copy (skeleton: Skeleton): T; + + abstract sort (skeleton: Skeleton): void; + + abstract update (skeleton: Skeleton, physics: Physics): void; + + isSourceActive (): boolean { + return true; + } +} diff --git a/spine-ts/spine-core/src/ConstraintData.ts b/spine-ts/spine-core/src/ConstraintData.ts index 01b404236..4a55a2604 100644 --- a/spine-ts/spine-core/src/ConstraintData.ts +++ b/spine-ts/spine-core/src/ConstraintData.ts @@ -27,7 +27,20 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ +import { Constraint } from "./Constraint"; +import { Pose } from "./Pose"; +import { PosedData } from "./PosedData"; +import { Skeleton } from "./Skeleton"; + /** The base class for all constraint datas. */ -export abstract class ConstraintData { - constructor (public name: string, public order: number, public skinRequired: boolean) { } +export abstract class ConstraintData< + T extends Constraint, + P extends Pose> + extends PosedData

{ + + constructor (name: string, setup: P) { + super(name, setup); + } + + abstract create (skeleton: Skeleton): T; } diff --git a/spine-ts/spine-core/src/Event.ts b/spine-ts/spine-core/src/Event.ts index 65362d365..67327d231 100644 --- a/spine-ts/spine-core/src/Event.ts +++ b/spine-ts/spine-core/src/Event.ts @@ -29,13 +29,16 @@ import { EventData } from "./EventData.js"; +import type { Timeline } from "./Animation.js"; +import type { AnimationStateListener } from "./AnimationState.js"; + /** Stores the current pose values for an {@link Event}. * - * See Timeline {@link Timeline#apply()}, - * AnimationStateListener {@link AnimationStateListener#event()}, and + * See Timeline {@link Timeline.apply()}, + * AnimationStateListener {@link AnimationStateListener.event()}, and * [Events](http://esotericsoftware.com/spine-events) in the Spine User Guide. */ export class Event { - data: EventData; + readonly data: EventData; intValue: number = 0; floatValue: number = 0; stringValue: string | null = null; diff --git a/spine-ts/spine-core/src/IkConstraint.ts b/spine-ts/spine-core/src/IkConstraint.ts index 4f8f08377..6f7e5f0a4 100644 --- a/spine-ts/spine-core/src/IkConstraint.ts +++ b/spine-ts/spine-core/src/IkConstraint.ts @@ -29,109 +29,109 @@ import { Bone } from "./Bone.js"; import { Inherit } from "./BoneData.js"; +import { BonePose } from "./BonePose.js"; +import { Constraint } from "./Constraint.js"; import { IkConstraintData } from "./IkConstraintData.js"; -import { Physics, Skeleton } from "./Skeleton.js"; -import { Updatable } from "./Updatable.js"; +import { IkConstraintPose } from "./IkConstraintPose.js"; +import { Physics } from "./Physics.js"; +import { 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. * * See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */ -export class IkConstraint implements Updatable { - /** The IK constraint's setup pose data. */ - data: IkConstraintData; - - /** The bones that will be modified by this IK constraint. */ - bones: Array; +export class IkConstraint extends Constraint { + /** The 1 or 2 bones that will be modified by this IK constraint. */ + readonly bones: Array; /** The bone that is the IK target. */ target: Bone; - /** Controls the bend direction of the IK bones, either 1 or -1. */ - bendDirection = 0; - - /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ - compress = false; - - /** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained - * and 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 rotations. */ - mix = 1; - - /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ - softness = 0; - active = false; - constructor (data: IkConstraintData, skeleton: Skeleton) { - if (!data) throw new Error("data cannot be null."); + super(data, new IkConstraintPose(), new IkConstraintPose()); if (!skeleton) throw new Error("skeleton cannot be null."); - this.data = data; - this.bones = new Array(); - for (let i = 0; i < data.bones.length; i++) { - let bone = skeleton.findBone(data.bones[i].name); - if (!bone) throw new Error(`Couldn't find bone ${data.bones[i].name}`); - this.bones.push(bone); - } - let target = skeleton.findBone(data.target.name); - if (!target) throw new Error(`Couldn't find bone ${data.target.name}`); + this.bones = new Array(); + for (const boneData of data.bones) + this.bones.push(skeleton.bones[boneData.index].constrained); - this.target = target; - this.mix = data.mix; - this.softness = data.softness; - this.bendDirection = data.bendDirection; - this.compress = data.compress; - this.stretch = data.stretch; + this.target = skeleton.bones[data.target.index]; } - isActive () { - return this.active; + copy (skeleton: Skeleton): IkConstraint { + var copy = new IkConstraint(this.data, skeleton); + copy.pose.set(this.pose); + return copy; } - setToSetupPose () { - const data = this.data; - this.mix = data.mix; - this.softness = data.softness; - this.bendDirection = data.bendDirection; - this.compress = data.compress; - this.stretch = data.stretch; - } - - update (physics: Physics) { - if (this.mix == 0) return; - let target = this.target; + update (skeleton: Skeleton, physics: Physics) { + const p = this.applied; + if (p.mix === 0) return; + let target = this.target.applied; let bones = this.bones; switch (bones.length) { case 1: - this.apply1(bones[0], target.worldX, target.worldY, this.compress, this.stretch, this.data.uniform, this.mix); + IkConstraint.apply(skeleton, bones[0], target.worldX, target.worldY, p.compress, p.stretch, this.data.uniform, p.mix); break; case 2: - this.apply2(bones[0], bones[1], target.worldX, target.worldY, this.bendDirection, this.stretch, this.data.uniform, this.softness, this.mix); + IkConstraint.apply(skeleton, bones[0], bones[1], target.worldX, target.worldY, p.bendDirection, p.stretch, this.data.uniform, + p.softness, p.mix); break; } } + sort (skeleton: Skeleton) { + skeleton.sortBone(this.target); + const parent = this.bones[0].bone; + skeleton.sortBone(parent); + skeleton._updateCache.push(this); + parent.sorted = false; + skeleton.sortReset(parent.children); + skeleton.constrained(parent); + if (this.bones.length > 1) skeleton.constrained(this.bones[1].bone); + } + + isSourceActive () { + return this.target.active; + } + /** Applies 1 bone IK. The target is specified in the world coordinate system. */ - apply1 (bone: Bone, targetX: number, targetY: number, compress: boolean, stretch: boolean, uniform: boolean, alpha: number) { - let p = bone.parent; - if (!p) throw new Error("IK bone must have parent."); + public static apply (skeleton: Skeleton, bone: BonePose, targetX: number, targetY: number, compress: boolean, stretch: boolean, uniform: boolean, mix: number): void; + + /** Applies 2 bone IK. The target is specified in the world coordinate system. + * @param child A direct descendant of the parent bone. */ + public static apply (skeleton: Skeleton, parent: BonePose, child: BonePose, targetX: number, targetY: number, bendDir: number, stretch: boolean, uniform: boolean, softness: number, mix: number): void; + + public static apply (skeleton: Skeleton, boneOrParent: BonePose, targetXorChild: number | BonePose, targetYOrTargetX: number, compressOrTargetY: boolean | number, + stretchOrBendDir: boolean | number, uniformOrStretch: boolean, mixOrUniform: number | boolean, softness?: number, mix?: number) { + + if (typeof targetXorChild === "number") + this.apply1(skeleton, boneOrParent, targetXorChild, targetYOrTargetX, compressOrTargetY as boolean, stretchOrBendDir as boolean, uniformOrStretch, mixOrUniform as number); + else + this.apply2(skeleton, boneOrParent, targetXorChild as BonePose, targetYOrTargetX, compressOrTargetY as number, stretchOrBendDir as number, + uniformOrStretch, mixOrUniform as boolean, softness as number, mix as number); + } + + private static apply1 (skeleton: Skeleton, bone: BonePose, targetX: number, targetY: number, compress: boolean, stretch: boolean, uniform: boolean, mix: number) { + bone.modifyLocal(skeleton); + + let p = bone.bone.parent!.applied; + let pa = p.a, pb = p.b, pc = p.c, pd = p.d; - let rotationIK = -bone.ashearX - bone.arotation, tx = 0, ty = 0; + let rotationIK = -bone.shearX - bone.rotation, tx = 0, ty = 0; switch (bone.inherit) { case Inherit.OnlyTranslation: - tx = (targetX - bone.worldX) * MathUtils.signum(bone.skeleton.scaleX); - ty = (targetY - bone.worldY) * MathUtils.signum(bone.skeleton.scaleY); + tx = (targetX - bone.worldX) * MathUtils.signum(skeleton.scaleX); + ty = (targetY - bone.worldY) * MathUtils.signum(skeleton.scaleY); break; case Inherit.NoRotationOrReflection: let s = Math.abs(pa * pd - pb * pc) / Math.max(0.0001, pa * pa + pc * pc); - let sa = pa / bone.skeleton.scaleX; - let sc = pc / bone.skeleton.scaleY; - pb = -sc * s * bone.skeleton.scaleX; - pd = sa * s * bone.skeleton.scaleY; + let sa = pa / skeleton.scaleX; + let sc = pc / skeleton.scaleY; + pb = -sc * s * skeleton.scaleX; + pd = sa * s * skeleton.scaleY; rotationIK += Math.atan2(sc, sa) * MathUtils.radDeg; // Fall through default: @@ -141,17 +141,17 @@ export class IkConstraint implements Updatable { tx = 0; ty = 0; } else { - tx = (x * pd - y * pb) / d - bone.ax; - ty = (y * pa - x * pc) / d - bone.ay; + tx = (x * pd - y * pb) / d - bone.x; + ty = (y * pa - x * pc) / d - bone.y; } } - rotationIK += Math.atan2(ty, tx) * MathUtils.radDeg; - if (bone.ascaleX < 0) rotationIK += 180; + rotationIK += MathUtils.atan2Deg(ty, tx); + if (bone.scaleX < 0) rotationIK += 180; if (rotationIK > 180) rotationIK -= 360; else if (rotationIK < -180) rotationIK += 360; - let sx = bone.ascaleX, sy = bone.ascaleY; + bone.rotation += rotationIK * mix; if (compress || stretch) { switch (bone.inherit) { case Inherit.NoScale: @@ -159,25 +159,25 @@ export class IkConstraint implements Updatable { tx = targetX - bone.worldX; ty = targetY - bone.worldY; } - const b = bone.data.length * sx; + const b = bone.bone.data.length * bone.scaleX; if (b > 0.0001) { const dd = tx * tx + ty * ty; if ((compress && dd < b * b) || (stretch && dd > b * b)) { - const s = (Math.sqrt(dd) / b - 1) * alpha + 1; - sx *= s; - if (uniform) sy *= s; + const s = (Math.sqrt(dd) / b - 1) * mix + 1; + bone.scaleX *= s; + if (uniform) bone.scaleY *= s; } } } - bone.updateWorldTransformWith(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, - bone.ashearY); } /** Applies 2 bone IK. The target is specified in the world coordinate system. * @param child A direct descendant of the parent bone. */ - apply2 (parent: Bone, child: Bone, targetX: number, targetY: number, bendDir: number, stretch: boolean, uniform: boolean, softness: number, alpha: number) { + private static apply2 (skeleton: Skeleton, parent: BonePose, child: BonePose, targetX: number, targetY: number, bendDir: number, stretch: boolean, uniform: boolean, softness: number, mix: number) { if (parent.inherit != Inherit.Normal || child.inherit != Inherit.Normal) return; - let px = parent.ax, py = parent.ay, psx = parent.ascaleX, psy = parent.ascaleY, sx = psx, sy = psy, csx = child.ascaleX; + parent.modifyLocal(skeleton); + child.modifyLocal(skeleton); + let px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, sx = psx, sy = psy, csx = child.scaleX; let os1 = 0, os2 = 0, s2 = 0; if (psx < 0) { psx = -psx; @@ -196,19 +196,17 @@ export class IkConstraint implements Updatable { os2 = 180; } else os2 = 0; - let cx = child.ax, cy = 0, cwx = 0, cwy = 0, a = parent.a, b = parent.b, c = parent.c, d = parent.d; + let cwx = 0, cwy = 0, a = parent.a, b = parent.b, c = parent.c, d = parent.d; let u = Math.abs(psx - psy) <= 0.0001; if (!u || stretch) { - cy = 0; - cwx = a * cx + parent.worldX; - cwy = c * cx + parent.worldY; + child.y = 0; + cwx = a * child.x + parent.worldX; + cwy = c * child.x + parent.worldY; } else { - cy = child.ay; - cwx = a * cx + b * cy + parent.worldX; - cwy = c * cx + d * cy + parent.worldY; + cwx = a * child.x + b * child.y + parent.worldX; + cwy = c * child.x + d * child.y + parent.worldY; } - let pp = parent.parent; - if (!pp) throw new Error("IK parent must itself have a parent."); + let pp = parent.bone.parent!.applied; a = pp.a; b = pp.b; c = pp.c; @@ -216,10 +214,10 @@ export class IkConstraint implements Updatable { let id = a * d - b * c, x = cwx - pp.worldX, y = cwy - pp.worldY; id = Math.abs(id) <= 0.0001 ? 0 : 1 / id; let dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py; - let l1 = Math.sqrt(dx * dx + dy * dy), l2 = child.data.length * csx, a1, a2; + let l1 = Math.sqrt(dx * dx + dy * dy), l2 = child.bone.data.length * csx, a1, a2; if (l1 < 0.0001) { - this.apply1(parent, targetX, targetY, false, stretch, false, alpha); - child.updateWorldTransformWith(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + IkConstraint.apply(skeleton, parent, targetX, targetY, false, stretch, false, mix); + child.rotation = 0; return; } x = targetX - pp.worldX; @@ -248,9 +246,9 @@ export class IkConstraint implements Updatable { cos = 1; a2 = 0; if (stretch) { - a = (Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1; - sx *= a; - if (uniform) sy *= a; + a = (Math.sqrt(dd) / (l1 + l2) - 1) * mix + 1; + parent.scaleX *= a; + if (uniform) parent.scaleY *= a; } } else a2 = Math.acos(cos) * bendDir; @@ -307,20 +305,18 @@ export class IkConstraint implements Updatable { a2 = maxAngle * bendDir; } } - let os = Math.atan2(cy, cx) * s2; - let rotation = parent.arotation; - a1 = (a1 - os) * MathUtils.radDeg + os1 - rotation; + let os = Math.atan2(child.y, child.x) * s2; + a1 = (a1 - os) * MathUtils.radDeg + os1 - parent.rotation; if (a1 > 180) a1 -= 360; else if (a1 < -180) // a1 += 360; - parent.updateWorldTransformWith(px, py, rotation + a1 * alpha, sx, sy, 0, 0); - rotation = child.arotation; - a2 = ((a2 + os) * MathUtils.radDeg - child.ashearX) * s2 + os2 - rotation; + parent.rotation += a1 * mix; + a2 = ((a2 + os) * MathUtils.radDeg - child.shearX) * s2 + os2 - child.rotation; if (a2 > 180) a2 -= 360; else if (a2 < -180) // a2 += 360; - child.updateWorldTransformWith(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY); + child.rotation += a2 * mix; } } diff --git a/spine-ts/spine-core/src/IkConstraintData.ts b/spine-ts/spine-core/src/IkConstraintData.ts index fe594b8c2..af55ed365 100644 --- a/spine-ts/spine-core/src/IkConstraintData.ts +++ b/spine-ts/spine-core/src/IkConstraintData.ts @@ -29,44 +29,34 @@ import { BoneData } from "./BoneData.js"; import { ConstraintData } from "./ConstraintData.js"; - +import { IkConstraint } from "./IkConstraint.js"; +import { IkConstraintPose } from "./IkConstraintPose.js"; +import { Skeleton } from "./Skeleton.js"; /** Stores the setup pose for an {@link IkConstraint}. - *

+ * * See [IK constraints](http://esotericsoftware.com/spine-ik-constraints) in the Spine User Guide. */ -export class IkConstraintData extends ConstraintData { +export class IkConstraintData extends ConstraintData { /** The bones that are constrained by this IK constraint. */ bones = new Array(); - /** The bone that is the IK target. */ private _target: BoneData | null = null; + /** The bone that is the IK target. */ public set target (boneData: BoneData) { this._target = boneData; } public get target () { - if (!this._target) throw new Error("BoneData not set.") + if (!this._target) throw new Error("target cannot be null.") else return this._target; } - /** Controls the bend direction of the IK bones, either 1 or -1. */ - bendDirection = 0; - - /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ - compress = false; - - /** When true, if the target is out of range, the parent bone is scaled to reach it. If more than one bone is being constrained - * and the parent bone has local nonuniform scale, stretch is not applied. */ - stretch = false; - - /** When true, only a single bone is being constrained, and {@link #getCompress()} or {@link #getStretch()} is used, the bone - * is scaled on both the X and Y axes. */ + /** When true and {@link IkConstraintPose.compress} or {@link IkConstraintPose.stretch} is used, the bone is scaled + * on both the X and Y axes. */ uniform = false; - /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ - mix = 0; - - /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ - softness = 0; - constructor (name: string) { - super(name, 0, false); + super(name, new IkConstraintPose()); + } + + public create (skeleton: Skeleton) { + return new IkConstraint(this, skeleton); } } diff --git a/spine-ts/spine-core/src/IkConstraintPose.ts b/spine-ts/spine-core/src/IkConstraintPose.ts new file mode 100644 index 000000000..60f7e24b1 --- /dev/null +++ b/spine-ts/spine-core/src/IkConstraintPose.ts @@ -0,0 +1,62 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose"; + +/** Stores the current pose for an IK constraint. */ +export class IkConstraintPose implements Pose { + /** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */ + bendDirection = 0; + + /** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */ + 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. */ + stretch = false; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. + * + * For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */ + mix = 0; + + /** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones + * will not straighten completely until the target is this far out of range. */ + softness = 0; + + public set (pose: IkConstraintPose) { + this.mix = pose.mix; + this.softness = pose.softness; + this.bendDirection = pose.bendDirection; + this.compress = pose.compress; + this.stretch = pose.stretch; + } +} diff --git a/spine-ts/spine-core/src/PathConstraint.ts b/spine-ts/spine-core/src/PathConstraint.ts index bfd0bfdbf..6923d0ebf 100644 --- a/spine-ts/spine-core/src/PathConstraint.ts +++ b/spine-ts/spine-core/src/PathConstraint.ts @@ -27,12 +27,17 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ +import { Attachment } from "./attachments/Attachment.js"; import { PathAttachment } from "./attachments/PathAttachment.js"; import { Bone } from "./Bone.js"; +import { BonePose } from "./BonePose.js"; +import { Constraint } from "./Constraint.js"; import { PathConstraintData, RotateMode, SpacingMode, PositionMode } from "./PathConstraintData.js"; -import { Physics, Skeleton } from "./Skeleton.js"; +import { PathConstraintPose } from "./PathConstraintPose.js"; +import { Physics } from "./Physics.js"; +import { Skeleton } from "./Skeleton.js"; +import { Skin, SkinEntry } from "./Skin.js"; import { Slot } from "./Slot.js"; -import { Updatable } from "./Updatable.js"; import { Utils, MathUtils } from "./Utils.js"; @@ -40,7 +45,7 @@ import { Utils, MathUtils } from "./Utils.js"; * 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 implements Updatable { +export class PathConstraint extends Constraint { static NONE = -1; static BEFORE = -2; static AFTER = -3; static epsilon = 0.00001; @@ -48,70 +53,40 @@ export class PathConstraint implements Updatable { data: PathConstraintData; /** The bones that will be modified by this path constraint. */ - bones: Array; + bones: Array; /** The slot whose path attachment will be used to constrained the bones. */ slot: Slot; - /** The position along the path. */ - position = 0; - - /** The spacing between bones. */ - spacing = 0; - - mixRotate = 0; - - mixX = 0; - - mixY = 0; - spaces = new Array(); positions = new Array(); world = new Array(); curves = new Array(); lengths = new Array(); segments = new Array(); - active = false; - constructor (data: PathConstraintData, skeleton: Skeleton) { - if (!data) throw new Error("data cannot be null."); + super(data, new PathConstraintPose(), new PathConstraintPose()); if (!skeleton) throw new Error("skeleton cannot be null."); this.data = data; - this.bones = new Array(); - for (let i = 0, n = data.bones.length; i < n; i++) { - let bone = skeleton.findBone(data.bones[i].name); - if (!bone) throw new Error(`Couldn't find bone ${data.bones[i].name}.`); - this.bones.push(bone); - } - let target = skeleton.findSlot(data.slot.name); - if (!target) throw new Error(`Couldn't find target bone ${data.slot.name}`); - this.slot = target; + this.bones = new Array(); + for (const boneData of this.data.bones) + this.bones.push(skeleton.bones[boneData.index].constrained); - this.position = data.position; - this.spacing = data.spacing; - this.mixRotate = data.mixRotate; - this.mixX = data.mixX; - this.mixY = data.mixY; + this.slot = skeleton.slots[data.slot.index];; } - isActive () { - return this.active; + public copy (skeleton: Skeleton) { + var copy = new PathConstraint(this.data, skeleton); + copy.pose.set(this.pose); + return copy; } - setToSetupPose () { - const data = this.data; - this.position = data.position; - this.spacing = data.spacing; - this.mixRotate = data.mixRotate; - this.mixX = data.mixX; - this.mixY = data.mixY; - } - - update (physics: Physics) { - let attachment = this.slot.getAttachment(); + update (skeleton: Skeleton, physics: Physics) { + let attachment = this.slot.applied.attachment; if (!(attachment instanceof PathAttachment)) return; - let mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY; - if (mixRotate == 0 && mixX == 0 && mixY == 0) return; + const p = this.applied; + let mixRotate = p.mixRotate, mixX = p.mixX, mixY = p.mixY; + if (mixRotate === 0 && mixX === 0 && mixY === 0) return; let data = this.data; let tangents = data.rotateMode == RotateMode.Tangent, scale = data.rotateMode == RotateMode.ChainScale; @@ -119,14 +94,14 @@ export class PathConstraint implements Updatable { let bones = this.bones; let boneCount = bones.length, spacesCount = tangents ? boneCount : boneCount + 1; let spaces = Utils.setArraySize(this.spaces, spacesCount), lengths: Array = scale ? this.lengths = Utils.setArraySize(this.lengths, boneCount) : []; - let spacing = this.spacing; + let spacing = p.spacing; switch (data.spacingMode) { case SpacingMode.Percent: if (scale) { for (let i = 0, n = spacesCount - 1; i < n; i++) { let bone = bones[i]; - let setupLength = bone.data.length; + let setupLength = bone.bone.data.length; let x = setupLength * bone.a, y = setupLength * bone.c; lengths[i] = Math.sqrt(x * x + y * y); } @@ -137,7 +112,7 @@ export class PathConstraint implements Updatable { let sum = 0; for (let i = 0, n = spacesCount - 1; i < n;) { let bone = bones[i]; - let setupLength = bone.data.length; + let setupLength = bone.bone.data.length; if (setupLength < PathConstraint.epsilon) { if (scale) lengths[i] = 0; spaces[++i] = spacing; @@ -159,7 +134,7 @@ export class PathConstraint implements Updatable { let lengthSpacing = data.spacingMode == SpacingMode.Length; for (let i = 0, n = spacesCount - 1; i < n;) { let bone = bones[i]; - let setupLength = bone.data.length; + let setupLength = bone.bone.data.length; if (setupLength < PathConstraint.epsilon) { if (scale) lengths[i] = 0; spaces[++i] = spacing; @@ -167,26 +142,26 @@ export class PathConstraint implements Updatable { let x = setupLength * bone.a, y = setupLength * bone.c; let length = Math.sqrt(x * x + y * y); if (scale) lengths[i] = length; - spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength; + spaces[++i] = (lengthSpacing ? Math.max(0, setupLength + spacing) : spacing) * length / setupLength; } } } - let positions = this.computeWorldPositions(attachment, spacesCount, tangents); + let positions = this.computeWorldPositions(skeleton, attachment, spacesCount, tangents); let boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation; let tip = false; if (offsetRotation == 0) tip = data.rotateMode == RotateMode.Chain; else { tip = false; - let p = this.slot.bone; - offsetRotation *= p.a * p.d - p.b * p.c > 0 ? MathUtils.degRad : -MathUtils.degRad; + let bone = this.slot.bone.applied; + offsetRotation *= bone.a * bone.d - bone.b * bone.c > 0 ? MathUtils.degRad : -MathUtils.degRad; } - for (let i = 0, p = 3; i < boneCount; i++, p += 3) { + for (let i = 0, ip = 3, u = skeleton._update; i < boneCount; i++, ip += 3) { let bone = bones[i]; bone.worldX += (boneX - bone.worldX) * mixX; bone.worldY += (boneY - bone.worldY) * mixY; - let x = positions[p], y = positions[p + 1], dx = x - boneX, dy = y - boneY; + let x = positions[ip], y = positions[ip + 1], dx = x - boneX, dy = y - boneY; if (scale) { let length = lengths[i]; if (length != 0) { @@ -200,16 +175,16 @@ export class PathConstraint implements Updatable { if (mixRotate > 0) { let a = bone.a, b = bone.b, c = bone.c, d = bone.d, r = 0, cos = 0, sin = 0; if (tangents) - r = positions[p - 1]; + r = positions[ip - 1]; else if (spaces[i + 1] == 0) - r = positions[p + 2]; + r = positions[ip + 2]; else r = Math.atan2(dy, dx); r -= Math.atan2(c, a); if (tip) { cos = Math.cos(r); sin = Math.sin(r); - let length = bone.data.length; + let length = bone.bone.data.length; boneX += (length * (cos * a - sin * c) - dx) * mixRotate; boneY += (length * (sin * a + cos * c) - dy) * mixRotate; } else { @@ -227,13 +202,13 @@ export class PathConstraint implements Updatable { bone.c = sin * a + cos * c; bone.d = sin * b + cos * d; } - bone.updateAppliedTransform(); + bone.modifyWorld(u); } } - computeWorldPositions (path: PathAttachment, spacesCount: number, tangents: boolean) { + computeWorldPositions (skeleton: Skeleton, path: PathAttachment, spacesCount: number, tangents: boolean) { let slot = this.slot; - let position = this.position; + let position = this.applied.position; let spaces = this.spaces, out = Utils.setArraySize(this.positions, spacesCount * 3 + 2), world: Array = this.world; let closed = path.closed; let verticesLength = path.worldVerticesLength, curveCount = verticesLength / 6, prevCurve = PathConstraint.NONE; @@ -246,15 +221,11 @@ export class PathConstraint implements Updatable { let multiplier; switch (this.data.spacingMode) { - case SpacingMode.Percent: - multiplier = pathLength; - break; - case SpacingMode.Proportional: - multiplier = pathLength / spacesCount; - break; - default: - multiplier = 1; + case SpacingMode.Percent: multiplier = pathLength; break; + case SpacingMode.Proportional: multiplier = pathLength / spacesCount; break; + default: multiplier = 1; } + world = Utils.setArraySize(this.world, 8); for (let i = 0, o = 0, curve = 0; i < spacesCount; i++, o += 3) { let space = spaces[i] * multiplier; @@ -268,14 +239,14 @@ export class PathConstraint implements Updatable { } else if (p < 0) { if (prevCurve != PathConstraint.BEFORE) { prevCurve = PathConstraint.BEFORE; - path.computeWorldVertices(slot, 2, 4, world, 0, 2); + path.computeWorldVertices(skeleton, slot, 2, 4, world, 0, 2); } this.addBeforePosition(p, world, 0, out, o); continue; } else if (p > pathLength) { if (prevCurve != PathConstraint.AFTER) { prevCurve = PathConstraint.AFTER; - path.computeWorldVertices(slot, verticesLength - 6, 4, world, 0, 2); + path.computeWorldVertices(skeleton, slot, verticesLength - 6, 4, world, 0, 2); } this.addAfterPosition(p - pathLength, world, 0, out, o); continue; @@ -296,10 +267,10 @@ export class PathConstraint implements Updatable { if (curve != prevCurve) { prevCurve = curve; if (closed && curve == curveCount) { - path.computeWorldVertices(slot, verticesLength - 4, 4, world, 0, 2); - path.computeWorldVertices(slot, 0, 4, world, 4, 2); + path.computeWorldVertices(skeleton, slot, verticesLength - 4, 4, world, 0, 2); + path.computeWorldVertices(skeleton, slot, 0, 4, world, 4, 2); } else - path.computeWorldVertices(slot, curve * 6 + 2, 8, world, 0, 2); + path.computeWorldVertices(skeleton, slot, curve * 6 + 2, 8, world, 0, 2); } this.addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o, tangents || (i > 0 && space == 0)); @@ -311,15 +282,15 @@ export class PathConstraint implements Updatable { if (closed) { verticesLength += 2; world = Utils.setArraySize(this.world, verticesLength); - path.computeWorldVertices(slot, 2, verticesLength - 4, world, 0, 2); - path.computeWorldVertices(slot, 0, 2, world, verticesLength - 4, 2); + path.computeWorldVertices(skeleton, slot, 2, verticesLength - 4, world, 0, 2); + path.computeWorldVertices(skeleton, slot, 0, 2, world, verticesLength - 4, 2); world[verticesLength - 2] = world[0]; world[verticesLength - 1] = world[1]; } else { curveCount--; verticesLength -= 4; world = Utils.setArraySize(this.world, verticesLength); - path.computeWorldVertices(slot, 2, verticesLength, world, 0, 2); + path.computeWorldVertices(skeleton, slot, 2, verticesLength, world, 0, 2); } // Curve lengths. @@ -363,14 +334,9 @@ export class PathConstraint implements Updatable { let multiplier; switch (this.data.spacingMode) { - case SpacingMode.Percent: - multiplier = pathLength; - break; - case SpacingMode.Proportional: - multiplier = pathLength / spacesCount; - break; - default: - multiplier = 1; + case SpacingMode.Percent: multiplier = pathLength; break; + case SpacingMode.Proportional: multiplier = pathLength / spacesCount; break; + default: multiplier = 1; } let segments = this.segments; @@ -384,6 +350,7 @@ export class PathConstraint implements Updatable { p %= pathLength; if (p < 0) p += pathLength; curve = 0; + segment = 0; } else if (p < 0) { this.addBeforePosition(p, world, 0, out, o); continue; @@ -498,4 +465,53 @@ export class PathConstraint implements Updatable { out[o + 2] = Math.atan2(y - (y1 * uu + cy1 * ut * 2 + cy2 * tt), x - (x1 * uu + cx1 * ut * 2 + cx2 * tt)); } } + + sort (skeleton: Skeleton) { + const slotIndex = this.slot.data.index; + const slotBone = this.slot.bone; + if (skeleton.skin != null) this.sortPathSlot(skeleton, skeleton.skin, slotIndex, slotBone); + if (skeleton.data.defaultSkin != null && skeleton.data.defaultSkin != skeleton.skin) + this.sortPathSlot(skeleton, skeleton.data.defaultSkin, slotIndex, slotBone); + this.sortPath(skeleton, this.slot.pose.attachment, slotBone); + const bones = this.bones; + const boneCount = this.bones.length; + for (let i = 0; i < boneCount; i++) { + const bone = bones[i].bone; + skeleton.sortBone(bone); + skeleton.constrained(bone); + } + skeleton._updateCache.push(this); + for (let i = 0; i < boneCount; i++) + skeleton.sortReset(bones[i].bone.children); + for (let i = 0; i < boneCount; i++) + bones[i].bone.sorted = true; + } + + private sortPathSlot (skeleton: Skeleton, skin: Skin, slotIndex: number, slotBone: Bone) { + const entries = skin.getAttachments(); + for (let i = 0, n = entries.length; i < n; i++) { + const entry = entries[i]; + if (entry.slotIndex == slotIndex) this.sortPath(skeleton, entry.attachment, slotBone); + } + } + + private sortPath (skeleton: Skeleton, attachment: Attachment | null, slotBone: Bone) { + if (!(attachment instanceof PathAttachment)) return; + const pathBones = attachment.bones; + if (pathBones == null) + skeleton.sortBone(slotBone); + else { + const bones = skeleton.bones; + for (let i = 0, n = pathBones.length; i < n;) { + let nn = pathBones[i++]; + nn += i; + while (i < nn) + skeleton.sortBone(bones[pathBones[i++]]); + } + } + } + + isSourceActive () { + return this.slot.bone.active; + } } diff --git a/spine-ts/spine-core/src/PathConstraintData.ts b/spine-ts/spine-core/src/PathConstraintData.ts index b8f403515..735c4c1d2 100644 --- a/spine-ts/spine-core/src/PathConstraintData.ts +++ b/spine-ts/spine-core/src/PathConstraintData.ts @@ -29,24 +29,26 @@ import { BoneData } from "./BoneData.js"; import { ConstraintData } from "./ConstraintData.js"; +import { PathConstraint } from "./PathConstraint.js"; +import { PathConstraintPose } from "./PathConstraintPose.js"; +import { Skeleton } from "./Skeleton.js"; import { SlotData } from "./SlotData.js"; /** Stores the setup pose for a {@link PathConstraint}. * * See [path constraints](http://esotericsoftware.com/spine-path-constraints) in the Spine User Guide. */ -export class PathConstraintData extends ConstraintData { - +export class PathConstraintData extends ConstraintData { /** The bones that will be modified by this path constraint. */ bones = new Array(); /** The slot whose path attachment will be used to constrained the bones. */ - private _slot: SlotData | null = null; public set slot (slotData: SlotData) { this._slot = slotData; } public get slot () { if (!this._slot) throw new Error("SlotData not set.") - else return this._slot; + else return this._slot; } + private _slot: SlotData | null = null; /** The mode for positioning the first bone on the path. */ positionMode: PositionMode = PositionMode.Fixed; @@ -60,18 +62,12 @@ export class PathConstraintData extends ConstraintData { /** An offset added to the constrained bone rotation. */ offsetRotation: number = 0; - /** The position along the path. */ - position: number = 0; - - /** The spacing between bones. */ - spacing: number = 0; - - mixRotate = 0; - mixX = 0; - mixY = 0; - constructor (name: string) { - super(name, 0, false); + super(name, new PathConstraintPose()); + } + + public create (skeleton: Skeleton) { + return new PathConstraint(this, skeleton); } } diff --git a/spine-ts/spine-core/src/PathConstraintPose.ts b/spine-ts/spine-core/src/PathConstraintPose.ts new file mode 100644 index 000000000..f53e35151 --- /dev/null +++ b/spine-ts/spine-core/src/PathConstraintPose.ts @@ -0,0 +1,57 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose" + +/** Stores a pose for a path constraint. */ +export class PathConstraintPose implements Pose { + /** The position along the path. */ + position: number = 0; + + /** The spacing between bones. */ + spacing: number = 0; + + /** A percentage (0-1) 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. */ + mixX = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */ + mixY = 0; + + public set (pose: PathConstraintPose) { + this.position = pose.position; + this.spacing = pose.spacing; + this.mixRotate = pose.mixRotate; + this.mixX = pose.mixX; + this.mixY = pose.mixY; + } + +} diff --git a/spine-ts/spine-core/src/Physics.ts b/spine-ts/spine-core/src/Physics.ts new file mode 100644 index 000000000..6175d0e26 --- /dev/null +++ b/spine-ts/spine-core/src/Physics.ts @@ -0,0 +1,43 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +/** Determines how physics and other non-deterministic updates are applied. */ +export enum Physics { + /** Physics are not updated or applied. */ + none, + + /** Physics are reset to the current pose. */ + reset, + + /** Physics are updated and the pose from physics is applied. */ + update, + + /** Physics are not updated but the pose from physics is applied. */ + pose +} diff --git a/spine-ts/spine-core/src/PhysicsConstraint.ts b/spine-ts/spine-core/src/PhysicsConstraint.ts index 0580a8047..490d9019f 100644 --- a/spine-ts/spine-core/src/PhysicsConstraint.ts +++ b/spine-ts/spine-core/src/PhysicsConstraint.ts @@ -27,27 +27,20 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Bone } from "./Bone.js"; +import { BonePose } from "./BonePose.js"; +import { Constraint } from "./Constraint.js"; +import { Physics } from "./Physics.js"; import { PhysicsConstraintData } from "./PhysicsConstraintData.js"; -import { Physics, Skeleton } from "./Skeleton.js"; -import { Updatable } from "./Updatable.js"; +import { PhysicsConstraintPose } from "./PhysicsConstraintPose.js"; +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. *

* See Physics constraints in the Spine User Guide. */ -export class PhysicsConstraint implements Updatable { - readonly data: PhysicsConstraintData; - bone: Bone; - - inertia = 0; - strength = 0; - damping = 0; - massInverse = 0; - wind = 0; - gravity = 0; - mix = 0; +export class PhysicsConstraint extends Constraint { + bone: BonePose; _reset = true; ux = 0; @@ -57,84 +50,86 @@ export class PhysicsConstraint implements Updatable { tx = 0; ty = 0; xOffset = 0; + xLag = 0; xVelocity = 0; yOffset = 0; + yLag = 0; yVelocity = 0; rotateOffset = 0; + rotateLag = 0; rotateVelocity = 0; scaleOffset = 0 + scaleLag = 0 scaleVelocity = 0; - - active = false; - - readonly skeleton: Skeleton; remaining = 0; lastTime = 0; constructor (data: PhysicsConstraintData, skeleton: Skeleton) { - this.data = data; - this.skeleton = skeleton; + super(data, new PhysicsConstraintPose(), new PhysicsConstraintPose()); + if (skeleton == null) throw new Error("skeleton cannot be null."); - let bone = skeleton.findBone(data.bone.name); - if (!bone) throw new Error(`Couldn't find bone ${data.bone.name}.`); - this.bone = skeleton.bones[data.bone.index]; - - this.inertia = data.inertia; - this.strength = data.strength; - this.damping = data.damping; - this.massInverse = data.massInverse; - this.wind = data.wind; - this.gravity = data.gravity; - this.mix = data.mix; + this.bone = skeleton.bones[data.bone.index].constrained; } - reset () { + public copy (skeleton: Skeleton) { + var copy = new PhysicsConstraint(this.data, skeleton); + copy.pose.set(this.pose); + return copy; + } + + reset (skeleton: Skeleton) { this.remaining = 0; - this.lastTime = this.skeleton.time; + this.lastTime = skeleton.time; this._reset = true; this.xOffset = 0; + this.xLag = 0; this.xVelocity = 0; this.yOffset = 0; + this.yLag = 0; this.yVelocity = 0; this.rotateOffset = 0; + this.rotateLag = 0; this.rotateVelocity = 0; this.scaleOffset = 0; + this.scaleLag = 0; this.scaleVelocity = 0; } - setToSetupPose () { - const data = this.data; - this.inertia = data.inertia; - this.strength = data.strength; - this.damping = data.damping; - this.massInverse = data.massInverse; - this.wind = data.wind; - this.gravity = data.gravity; - this.mix = data.mix; + /** Translates the physics constraint so next {@link update} forces are applied as if the bone moved an + * additional amount in world space. */ + translate (x: number, y: number) { + this.ux -= x; + this.uy -= y; + this.cx -= x; + this.cy -= y; } - isActive () { - return this.active; + /** Rotates the physics constraint so next {@link update} forces are applied as if the bone rotated around the + * specified point in world space. */ + rotate (x: number, y: number, degrees: number) { + const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r); + const dx = this.cx - x, dy = this.cy - y; + this.translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy); } /** Applies the constraint to the constrained bones. */ - update (physics: Physics) { - const mix = this.mix; - if (mix == 0) return; + update (skeleton: Skeleton, physics: Physics) { + const p = this.applied; + const mix = p.mix; + if (mix === 0) return; const x = this.data.x > 0, y = this.data.y > 0, rotateOrShearX = this.data.rotate > 0 || this.data.shearX > 0, scaleX = this.data.scaleX > 0; const bone = this.bone; - const l = bone.data.length; + let l = bone.bone.data.length, t = this.data.step, z = 0; switch (physics) { case Physics.none: return; case Physics.reset: - this.reset(); + this.reset(skeleton); // Fall through. case Physics.update: - const skeleton = this.skeleton; - const delta = Math.max(this.skeleton.time - this.lastTime, 0); + const delta = Math.max(skeleton.time - this.lastTime, 0), aa = this.remaining; this.remaining += delta; this.lastTime = skeleton.time; @@ -144,8 +139,8 @@ export class PhysicsConstraint implements Updatable { this.ux = bx; this.uy = by; } else { - let a = this.remaining, i = this.inertia, t = this.data.step, f = this.skeleton.data.referenceScale, d = -1; - let qx = this.data.limit * delta, qy = qx * Math.abs(skeleton.scaleY); + let a = this.remaining, i = p.inertia, f = skeleton.data.referenceScale, d = -1, m = 0, e = 0, qx = this.data.limit * delta, + qy = qx * Math.abs(skeleton.scaleY); qx *= Math.abs(skeleton.scaleX); if (x || y) { if (x) { @@ -159,28 +154,34 @@ export class PhysicsConstraint implements Updatable { this.uy = by; } if (a >= t) { - d = Math.pow(this.damping, 60 * t); - const m = this.massInverse * t, e = this.strength, w = this.wind * f * skeleton.scaleX, g = this.gravity * f * skeleton.scaleY; + let xs = this.xOffset, ys = this.yOffset; + d = Math.pow(p.damping, 60 * t); + m = t * p.massInverse; + e = p.strength; + let w = f * p.wind * skeleton.scaleX, g = f * p.gravity * skeleton.scaleY, + ax = w * skeleton.windX + g * skeleton.gravityX, ay = w * skeleton.windY + g * skeleton.gravityY; do { if (x) { - this.xVelocity += (w - this.xOffset * e) * m; + this.xVelocity += (ax - this.xOffset * e) * m; this.xOffset += this.xVelocity * t; this.xVelocity *= d; } if (y) { - this.yVelocity -= (g + this.yOffset * e) * m; + this.yVelocity -= (ay + this.yOffset * e) * m; this.yOffset += this.yVelocity * t; this.yVelocity *= d; } a -= t; } while (a >= t); + this.xLag = this.xOffset - xs; + this.yLag = this.yOffset - ys; } - if (x) bone.worldX += this.xOffset * mix * this.data.x; - if (y) bone.worldY += this.yOffset * mix * this.data.y; + z = Math.max(0, 1 - a / t); + if (x) bone.worldX += (this.xOffset - this.xLag * z) * mix * this.data.x; + if (y) bone.worldY += (this.yOffset - this.yLag * z) * mix * this.data.y; } if (rotateOrShearX || scaleX) { - let ca = Math.atan2(bone.c, bone.a), c = 0, s = 0, mr = 0; - let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY; + let ca = Math.atan2(bone.c, bone.a), c, s, mr = 0, dx = this.cx - bone.worldX, dy = this.cy - bone.worldY; if (dx > qx) dx = qx; else if (dx < -qx) // @@ -189,11 +190,13 @@ export class PhysicsConstraint implements Updatable { dy = qy; else if (dy < -qy) // dy = -qy; + a = this.remaining; if (rotateOrShearX) { mr = (this.data.rotate + this.data.shearX) * mix; - let r = Math.atan2(dy + this.ty, dx + this.tx) - ca - this.rotateOffset * mr; + z = this.rotateLag * Math.max(0, 1 - aa / t); + let r = Math.atan2(dy + this.ty, dx + this.tx) - ca - (this.rotateOffset - z) * mr; this.rotateOffset += (r - Math.ceil(r * MathUtils.invPI2 - 0.5) * MathUtils.PI2) * i; - r = this.rotateOffset * mr + ca; + r = (this.rotateOffset - z) * mr + ca; c = Math.cos(r); s = Math.sin(r); if (scaleX) { @@ -203,22 +206,28 @@ export class PhysicsConstraint implements Updatable { } else { c = Math.cos(ca); s = Math.sin(ca); - const r = l * bone.getWorldScaleX(); + let r = l * bone.getWorldScaleX() - this.scaleLag * Math.max(0, 1 - aa / t); if (r > 0) this.scaleOffset += (dx * c + dy * s) * i / r; } a = this.remaining; if (a >= t) { - if (d == -1) d = Math.pow(this.damping, 60 * t); - const m = this.massInverse * t, e = this.strength, w = this.wind, g = (Skeleton.yDown ? -this.gravity : this.gravity), h = l / f; + if (d == -1) { + d = Math.pow(p.damping, 60 * t); + m = t * p.massInverse; + e = p.strength; + } + let rs = this.rotateOffset, ss = this.scaleOffset, h = l / f, + ax = p.wind * skeleton.windX + p.gravity * skeleton.gravityX, + ay = p.wind * skeleton.windY + p.gravity * skeleton.gravityY; while (true) { a -= t; if (scaleX) { - this.scaleVelocity += (w * c - g * s - this.scaleOffset * e) * m; + this.scaleVelocity += (ax * c - ay * s - this.scaleOffset * e) * m; this.scaleOffset += this.scaleVelocity * t; this.scaleVelocity *= d; } if (rotateOrShearX) { - this.rotateVelocity -= ((w * s + g * c) * h + this.rotateOffset * e) * m; + this.rotateVelocity -= ((ax * s + ay * c) * h + this.rotateOffset * e) * m; this.rotateOffset += this.rotateVelocity * t; this.rotateVelocity *= d; if (a < t) break; @@ -228,7 +237,10 @@ export class PhysicsConstraint implements Updatable { } else if (a < t) // break; } + this.rotateLag = this.rotateOffset - rs; + this.scaleLag = this.scaleOffset - ss; } + z = Math.max(0, 1 - a / t); } this.remaining = a; } @@ -236,12 +248,13 @@ export class PhysicsConstraint implements Updatable { this.cy = bone.worldY; break; case Physics.pose: - if (x) bone.worldX += this.xOffset * mix * this.data.x; - if (y) bone.worldY += this.yOffset * mix * this.data.y; + z = Math.max(0, 1 - this.remaining / t); + if (x) bone.worldX += (this.xOffset - this.xLag * z) * mix * this.data.x; + if (y) bone.worldY += (this.yOffset - this.yLag * z) * mix * this.data.y; } if (rotateOrShearX) { - let o = this.rotateOffset * mix, s = 0, c = 0, a = 0; + let o = (this.rotateOffset - this.rotateLag * z) * mix, s = 0, c = 0, a = 0; if (this.data.shearX > 0) { let r = 0; if (this.data.rotate > 0) { @@ -271,7 +284,7 @@ export class PhysicsConstraint implements Updatable { } } if (scaleX) { - const s = 1 + this.scaleOffset * mix * this.data.scaleX; + const s = 1 + (this.scaleOffset - this.scaleLag * z) * mix * this.data.scaleX; bone.a *= s; bone.c *= s; } @@ -279,23 +292,19 @@ export class PhysicsConstraint implements Updatable { this.tx = l * bone.a; this.ty = l * bone.c; } - bone.updateAppliedTransform(); + bone.modifyWorld(skeleton._update); } - /** Translates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone moved an additional - * amount in world space. */ - translate (x: number, y: number) { - this.ux -= x; - this.uy -= y; - this.cx -= x; - this.cy -= y; + sort (skeleton: Skeleton) { + const bone = this.bone.bone; + skeleton.sortBone(bone); + skeleton._updateCache.push(this); + skeleton.sortReset(bone.children); + skeleton.constrained(bone); } - /** Rotates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone rotated around the - * specified point in world space. */ - rotate (x: number, y: number, degrees: number) { - const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r); - const dx = this.cx - x, dy = this.cy - y; - this.translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy); + isSourceActive () { + return this.bone.bone.active; } + } diff --git a/spine-ts/spine-core/src/PhysicsConstraintData.ts b/spine-ts/spine-core/src/PhysicsConstraintData.ts index 6ea55a534..b6f0fe901 100644 --- a/spine-ts/spine-core/src/PhysicsConstraintData.ts +++ b/spine-ts/spine-core/src/PhysicsConstraintData.ts @@ -29,19 +29,22 @@ import { BoneData } from "./BoneData.js"; import { ConstraintData } from "./ConstraintData.js"; +import { PhysicsConstraint } from "./PhysicsConstraint.js"; +import { PhysicsConstraintPose } from "./PhysicsConstraintPose.js"; +import { Skeleton } from "./Skeleton.js"; /** Stores the setup pose for a {@link PhysicsConstraint}. *

* See Physics constraints in the Spine User Guide. */ -export class PhysicsConstraintData extends ConstraintData { - private _bone: BoneData | null = null; +export class PhysicsConstraintData extends ConstraintData { /** The bone constrained by this physics constraint. */ public set bone (boneData: BoneData) { this._bone = boneData; } public get bone () { if (!this._bone) throw new Error("BoneData not set.") - else return this._bone; + else return this._bone; } + private _bone: BoneData | null = null; x = 0; y = 0; @@ -50,14 +53,6 @@ export class PhysicsConstraintData extends ConstraintData { shearX = 0; limit = 0; step = 0; - inertia = 0; - strength = 0; - damping = 0; - massInverse = 0; - wind = 0; - gravity = 0; - /** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */ - mix = 0; inertiaGlobal = false; strengthGlobal = false; dampingGlobal = false; @@ -67,6 +62,10 @@ export class PhysicsConstraintData extends ConstraintData { mixGlobal = false; constructor (name: string) { - super(name, 0, false); + super(name, new PhysicsConstraintPose()); + } + + public create (skeleton: Skeleton) { + return new PhysicsConstraint(this, skeleton); } } diff --git a/spine-ts/spine-core/src/PhysicsConstraintPose.ts b/spine-ts/spine-core/src/PhysicsConstraintPose.ts new file mode 100644 index 000000000..8aaefe21f --- /dev/null +++ b/spine-ts/spine-core/src/PhysicsConstraintPose.ts @@ -0,0 +1,54 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose" + +/** Stores a pose for a physics constraint. */ +export class PhysicsConstraintPose implements Pose { + inertia = 0; + strength = 0; + damping = 0; + massInverse = 0; + wind = 0; + gravity = 0; + /** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */ + mix = 0; + + public set (pose: PhysicsConstraintPose) { + this.inertia = pose.inertia; + this.strength = pose.strength; + this.damping = pose.damping; + this.massInverse = pose.massInverse; + this.wind = pose.wind; + this.gravity = pose.gravity; + this.mix = pose.mix; + } + + +} diff --git a/spine-ts/spine-core/src/Pose.ts b/spine-ts/spine-core/src/Pose.ts new file mode 100644 index 000000000..4b1ae76cf --- /dev/null +++ b/spine-ts/spine-core/src/Pose.ts @@ -0,0 +1,32 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +export interface Pose

{ + set (pose: P): void; +} diff --git a/spine-ts/spine-core/src/Posed.ts b/spine-ts/spine-core/src/Posed.ts new file mode 100644 index 000000000..9d112ac52 --- /dev/null +++ b/spine-ts/spine-core/src/Posed.ts @@ -0,0 +1,55 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose"; +import { PosedData } from "./PosedData"; + +export abstract class Posed< + D extends PosedData

, + P extends Pose, + A extends P> { + + /** The constraint's setup pose data. */ + readonly data: D; + readonly pose: P; + readonly constrained: A; + applied: A; + + constructor (data: D, pose: P, constrained: A) { + if (data == null) throw new Error("data cannot be null."); + this.data = data; + this.pose = pose; + this.constrained = constrained; + this.applied = pose as A; + } + + public setupPose (): void { + this.pose.set(this.data.setup); + } +} diff --git a/spine-ts/spine-core/src/PosedActive.ts b/spine-ts/spine-core/src/PosedActive.ts new file mode 100644 index 000000000..7fb891c8c --- /dev/null +++ b/spine-ts/spine-core/src/PosedActive.ts @@ -0,0 +1,59 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose"; +import { Posed } from "./Posed"; +import { PosedData } from "./PosedData"; + +import type { Skeleton } from "./Skeleton"; + +export abstract class PosedActive< + D extends PosedData

, + P extends Pose, + A extends P> + extends Posed { + + active = false; + + constructor (data: D, pose: P, constrained: A) { + 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() */ + public isActive (): boolean { + return this.active; + } +} diff --git a/spine-ts/spine-core/src/PosedData.ts b/spine-ts/spine-core/src/PosedData.ts new file mode 100644 index 000000000..942182210 --- /dev/null +++ b/spine-ts/spine-core/src/PosedData.ts @@ -0,0 +1,51 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose"; + +/** The base class for all constrained datas. */ +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; + + /** When true, {@link Skeleton.updateWorldTransform} only updates this constraint if the {@link Skeleton.skin} + * contains this constraint. + * + * See {@link Skin.constraints}. */ + skinRequired = false; + + constructor (name: string, setup: P) { + if (name == null) throw new Error("name cannot be null."); + this.name = name; + this.setup = setup; + } + +} diff --git a/spine-ts/spine-core/src/Skeleton.ts b/spine-ts/spine-core/src/Skeleton.ts index 7c90454a3..988d0938e 100644 --- a/spine-ts/spine-core/src/Skeleton.ts +++ b/spine-ts/spine-core/src/Skeleton.ts @@ -30,19 +30,18 @@ import { Attachment } from "./attachments/Attachment.js"; import { ClippingAttachment } from "./attachments/ClippingAttachment.js"; import { MeshAttachment } from "./attachments/MeshAttachment.js"; -import { PathAttachment } from "./attachments/PathAttachment.js"; import { RegionAttachment } from "./attachments/RegionAttachment.js"; import { Bone } from "./Bone.js"; -import { IkConstraint } from "./IkConstraint.js"; -import { PathConstraint } from "./PathConstraint.js"; +import { BonePose } from "./BonePose.js"; +import { Constraint } from "./Constraint.js"; +import { Physics } from "./Physics.js"; import { PhysicsConstraint } from "./PhysicsConstraint.js"; +import { Posed } from "./Posed.js"; import { SkeletonClipping } from "./SkeletonClipping.js"; import { SkeletonData } from "./SkeletonData.js"; import { Skin } from "./Skin.js"; import { Slot } from "./Slot.js"; -import { TransformConstraint } from "./TransformConstraint.js"; -import { Updatable } from "./Updatable.js"; -import { Color, Utils, MathUtils, Vector2, NumberArrayLike } from "./Utils.js"; +import { Color, Utils, Vector2, NumberArrayLike } from "./Utils.js"; /** Stores the current pose for a skeleton. * @@ -52,47 +51,44 @@ export class Skeleton { static yDown = false; /** The skeleton's setup pose data. */ - data: SkeletonData; + readonly data: SkeletonData; /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ - bones: Array; + readonly bones: Array; - /** The skeleton's slots in the setup pose draw order. */ - slots: Array; + /** The skeleton's slots. */ + 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 IK constraints. */ - ikConstraints: Array; - - /** The skeleton's transform constraints. */ - transformConstraints: Array; - - /** The skeleton's path constraints. */ - pathConstraints: Array; - + /** The skeleton's constraints. */ + readonly constraints: Array>; /** The skeleton's physics constraints. */ - physicsConstraints: Array; + readonly physics: Array; - /** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */ - _updateCache = new Array(); + /** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link updateCache()}. */ + readonly _updateCache = new Array(); + + readonly resetCache: Array> = new Array(); /** The skeleton's current skin. May be null. */ skin: Skin | null = null; /** The color to tint all the skeleton's attachments. */ - color: Color; + readonly color: Color; - /** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale - * inheritance. */ + /** Scales the entire skeleton on the X axis. + * + * Bones that do not inherit scale are still affected by this property. */ scaleX = 1; - /** Scales the entire skeleton on the Y axis. This affects all bones, even if the bone's transform mode disallows scale - * inheritance. */ private _scaleY = 1; + /** Scales the entire skeleton on the Y axis. + * + * Bones that do not inherit scale are still affected by this property. */ public get scaleY () { return Skeleton.yDown ? -this._scaleY : this._scaleY; } @@ -101,17 +97,28 @@ export class Skeleton { this._scaleY = scaleY; } - /** Sets the skeleton X position, which is added to the root bone worldX position. */ + /** Sets the skeleton X position, which is added to the root bone worldX position. + * + * Bones that do not inherit translation are still affected by this property. */ x = 0; - /** Sets the skeleton Y position, which is added to the root bone worldY position. */ + /** Sets the skeleton Y position, which is added to the root bone worldY position. + * + * 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}. - *

- * See {@link #update(float)}. */ + * + * See {@link _update()}. */ time = 0; + windX = 1; + windY = 0; + gravityX = 0; + gravityY = 1; + + _update = 0; + constructor (data: SkeletonData) { if (!data) throw new Error("data cannot be null."); this.data = data; @@ -121,10 +128,10 @@ export class Skeleton { let boneData = data.bones[i]; let bone: Bone; if (!boneData.parent) - bone = new Bone(boneData, this, null); + bone = new Bone(boneData, null); else { let parent = this.bones[boneData.parent.index]; - bone = new Bone(boneData, this, parent); + bone = new Bone(boneData, parent); parent.children.push(bone); } this.bones.push(bone); @@ -132,55 +139,45 @@ export class Skeleton { this.slots = new Array(); this.drawOrder = new Array(); - for (let i = 0; i < data.slots.length; i++) { - let slotData = data.slots[i]; - let bone = this.bones[slotData.boneData.index]; - let slot = new Slot(slotData, bone); + for (const slotData of this.data.slots) { + let slot = new Slot(slotData, this); this.slots.push(slot); this.drawOrder.push(slot); } - this.ikConstraints = new Array(); - for (let i = 0; i < data.ikConstraints.length; i++) { - let ikConstraintData = data.ikConstraints[i]; - this.ikConstraints.push(new IkConstraint(ikConstraintData, this)); - } - - this.transformConstraints = new Array(); - for (let i = 0; i < data.transformConstraints.length; i++) { - let transformConstraintData = data.transformConstraints[i]; - this.transformConstraints.push(new TransformConstraint(transformConstraintData, this)); - } - - this.pathConstraints = new Array(); - for (let i = 0; i < data.pathConstraints.length; i++) { - let pathConstraintData = data.pathConstraints[i]; - this.pathConstraints.push(new PathConstraint(pathConstraintData, this)); - } - - this.physicsConstraints = new Array(); - for (let i = 0; i < data.physicsConstraints.length; i++) { - let physicsConstraintData = data.physicsConstraints[i]; - this.physicsConstraints.push(new PhysicsConstraint(physicsConstraintData, this)); + this.physics = new Array(); + this.constraints = new Array>(); + for (const constraintData of this.data.constraints) { + const constraint = constraintData.create(this); + if (constraint instanceof PhysicsConstraint) this.physics.push(constraint); + this.constraints.push(constraint); } this.color = new Color(1, 1, 1, 1); + this.updateCache(); } - /** Caches information about bones and constraints. Must be called if the {@link #getSkin()} is modified or if bones, + /** 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. */ updateCache () { - let updateCache = this._updateCache; - updateCache.length = 0; + this._updateCache.length = 0; + this.resetCache.length = 0; + + let slots = this.slots; + for (let i = 0, n = slots.length; i < n; i++) { + const slot = slots[i]; + slot.applied = slot.pose; + } let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { + const boneCount = bones.length; + for (let i = 0, n = boneCount; i < n; i++) { let bone = bones[i]; bone.sorted = bone.data.skinRequired; bone.active = !bone.sorted; + bone.applied = bone.pose as BonePose; } - if (this.skin) { let skinBones = this.skin.bones; for (let i = 0, n = this.skin.bones.length; i < n; i++) { @@ -193,171 +190,39 @@ export class Skeleton { } } - // IK first, lowest hierarchy depth first. - let ikConstraints = this.ikConstraints; - let transformConstraints = this.transformConstraints; - let pathConstraints = this.pathConstraints; - let physicsConstraints = this.physicsConstraints; - let ikCount = ikConstraints.length, transformCount = transformConstraints.length, pathCount = pathConstraints.length, physicsCount = this.physicsConstraints.length; - let constraintCount = ikCount + transformCount + pathCount + physicsCount; - - outer: - for (let i = 0; i < constraintCount; i++) { - for (let ii = 0; ii < ikCount; ii++) { - let constraint = ikConstraints[ii]; - if (constraint.data.order == i) { - this.sortIkConstraint(constraint); - continue outer; - } - } - for (let ii = 0; ii < transformCount; ii++) { - let constraint = transformConstraints[ii]; - if (constraint.data.order == i) { - this.sortTransformConstraint(constraint); - continue outer; - } - } - for (let ii = 0; ii < pathCount; ii++) { - let constraint = pathConstraints[ii]; - if (constraint.data.order == i) { - this.sortPathConstraint(constraint); - continue outer; - } - } - for (let ii = 0; ii < physicsCount; ii++) { - const constraint = physicsConstraints[ii]; - if (constraint.data.order == i) { - this.sortPhysicsConstraint(constraint); - continue outer; - } - } + let constraints = this.constraints; + let n = this.constraints.length; + for (let i = 0; i < n; i++) { + const constraint = constraints[i]; + constraint.applied = constraint.pose; + } + for (let i = 0; i < n; i++) { + const constraint = constraints[i]; + constraint.active = constraint.isSourceActive() + && (!constraint.data.skinRequired || (this.skin != null && this.skin.constraints.includes(constraint.data))); + if (constraint.active) constraint.sort(this); } - for (let i = 0, n = bones.length; i < n; i++) + for (let i = 0; i < boneCount; i++) this.sortBone(bones[i]); - } - sortIkConstraint (constraint: IkConstraint) { - constraint.active = constraint.target.isActive() && (!constraint.data.skinRequired || (this.skin && Utils.contains(this.skin.constraints, constraint.data, true)))!; - if (!constraint.active) return; - - this.sortBone(constraint.target); - - let constrained = constraint.bones; - let parent = constrained[0]; - this.sortBone(parent); - - if (constrained.length == 1) { - this._updateCache.push(constraint); - this.sortReset(parent.children); - } else { - let child = constrained[constrained.length - 1]; - this.sortBone(child); - - this._updateCache.push(constraint); - - this.sortReset(parent.children); - child.sorted = true; - } - } - - sortPathConstraint (constraint: PathConstraint) { - constraint.active = constraint.slot.bone.isActive() - && (!constraint.data.skinRequired || (this.skin && Utils.contains(this.skin.constraints, constraint.data, true)))!; - if (!constraint.active) return; - - let slot = constraint.slot; - let slotIndex = slot.data.index; - let slotBone = slot.bone; - if (this.skin) this.sortPathConstraintAttachment(this.skin, slotIndex, slotBone); - if (this.data.defaultSkin && this.data.defaultSkin != this.skin) - this.sortPathConstraintAttachment(this.data.defaultSkin, slotIndex, slotBone); - for (let i = 0, n = this.data.skins.length; i < n; i++) - this.sortPathConstraintAttachment(this.data.skins[i], slotIndex, slotBone); - - this.sortPathConstraintAttachmentWith(slot.attachment, slotBone); - - let constrained = constraint.bones; - let boneCount = constrained.length; - for (let i = 0; i < boneCount; i++) - this.sortBone(constrained[i]); - - this._updateCache.push(constraint); - - for (let i = 0; i < boneCount; i++) - this.sortReset(constrained[i].children); - for (let i = 0; i < boneCount; i++) - constrained[i].sorted = true; - } - - sortTransformConstraint (constraint: TransformConstraint) { - constraint.active = constraint.source.isActive() && (!constraint.data.skinRequired || (this.skin && Utils.contains(this.skin.constraints, constraint.data, true)))!; - if (!constraint.active) return; - - this.sortBone(constraint.source); - - let constrained = constraint.bones; - let boneCount = constrained.length; - if (constraint.data.localSource) { - for (let i = 0; i < boneCount; i++) { - let child = constrained[i]; - this.sortBone(child.parent!); - this.sortBone(child); - } - } else { - for (let i = 0; i < boneCount; i++) { - this.sortBone(constrained[i]); - } + 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; } - this._updateCache.push(constraint); - - for (let i = 0; i < boneCount; i++) - this.sortReset(constrained[i].children); - for (let i = 0; i < boneCount; i++) - constrained[i].sorted = true; } - sortPathConstraintAttachment (skin: Skin, slotIndex: number, slotBone: Bone) { - let attachments = skin.attachments[slotIndex]; - if (!attachments) return; - for (let key in attachments) { - this.sortPathConstraintAttachmentWith(attachments[key], slotBone); + constrained (object: Posed) { + if (object.pose === object.applied) { + object.applied = object.constrained; + this.resetCache.push(object); } } - sortPathConstraintAttachmentWith (attachment: Attachment | null, slotBone: Bone) { - if (!(attachment instanceof PathAttachment)) return; - let pathBones = attachment.bones; - if (!pathBones) - this.sortBone(slotBone); - else { - let bones = this.bones; - for (let i = 0, n = pathBones.length; i < n;) { - let nn = pathBones[i++]; - nn += i; - while (i < nn) - this.sortBone(bones[pathBones[i++]]); - } - } - } - - sortPhysicsConstraint (constraint: PhysicsConstraint) { - const bone = constraint.bone; - constraint.active = bone.active && (!constraint.data.skinRequired || (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true))); - if (!constraint.active) return; - - this.sortBone(bone); - - this._updateCache.push(constraint); - - this.sortReset(bone.children); - bone.sorted = true; - } - sortBone (bone: Bone) { - if (!bone) return; - if (bone.sorted) return; + if (bone.sorted || !bone.active) return; let parent = bone.parent; if (parent) this.sortBone(parent); bone.sorted = true; @@ -367,149 +232,112 @@ export class Skeleton { sortReset (bones: Array) { for (let i = 0, n = bones.length; i < n; i++) { let bone = bones[i]; - if (!bone.active) continue; - if (bone.sorted) this.sortReset(bone.children); - bone.sorted = false; + if (!bone.active) { + if (bone.sorted) this.sortReset(bone.children); + bone.sorted = false; + } } } /** Updates the world transform for each bone and applies all constraints. - * - * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine + *

+ * See World transforms in the Spine * Runtimes Guide. */ - updateWorldTransform (physics: Physics) { - if (physics === undefined || physics === null) throw new Error("physics is undefined"); - let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - bone.ax = bone.x; - bone.ay = bone.y; - bone.arotation = bone.rotation; - bone.ascaleX = bone.scaleX; - bone.ascaleY = bone.scaleY; - bone.ashearX = bone.shearX; - bone.ashearY = bone.shearY; + updateWorldTransform(physics: Physics): void { + this._update++; + + const resetCache = this.resetCache; + for (let i = 0, n = this.resetCache.length; i < n; i++) { + const object = resetCache[i]; + object.applied.set(object.pose); } - let updateCache = this._updateCache; - for (let i = 0, n = updateCache.length; i < n; i++) - updateCache[i].update(physics); - } - - updateWorldTransformWith (physics: Physics, parent: Bone) { - if (!parent) throw new Error("parent cannot be null."); - - let bones = this.bones; - for (let i = 1, n = bones.length; i < n; i++) { // Skip root bone. - let bone = bones[i]; - bone.ax = bone.x; - bone.ay = bone.y; - bone.arotation = bone.rotation; - bone.ascaleX = bone.scaleX; - bone.ascaleY = bone.scaleY; - bone.ashearX = bone.shearX; - bone.ashearY = bone.shearY; - } - - // Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection. - let rootBone = this.getRootBone(); - if (!rootBone) throw new Error("Root bone must not be null."); - let pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d; - rootBone.worldX = pa * this.x + pb * this.y + parent.worldX; - rootBone.worldY = pc * this.x + pd * this.y + parent.worldY; - - const rx = (rootBone.rotation + rootBone.shearX) * MathUtils.degRad; - const ry = (rootBone.rotation + 90 + rootBone.shearY) * MathUtils.degRad; - const la = Math.cos(rx) * rootBone.scaleX; - const lb = Math.cos(ry) * rootBone.scaleY; - const lc = Math.sin(rx) * rootBone.scaleX; - const ld = Math.sin(ry) * rootBone.scaleY; - rootBone.a = (pa * la + pb * lc) * this.scaleX; - rootBone.b = (pa * lb + pb * ld) * this.scaleX; - rootBone.c = (pc * la + pd * lc) * this.scaleY; - rootBone.d = (pc * lb + pd * ld) * this.scaleY; - - // Update everything except root bone. - let updateCache = this._updateCache; - for (let i = 0, n = updateCache.length; i < n; i++) { - let updatable = updateCache[i]; - if (updatable != rootBone) updatable.update(physics); - } + const updateCache = this._updateCache; + for (let i = 0, n = this._updateCache.length; i < n; i++) + updateCache[i].update(this, physics); } /** Sets the bones, constraints, and slots to their setup pose values. */ - setToSetupPose () { - this.setBonesToSetupPose(); - this.setSlotsToSetupPose(); + setupPose () { + this.setupPoseBones(); + this.setupPoseSlots(); } /** Sets the bones and constraints to their setup pose values. */ - setBonesToSetupPose () { - for (const bone of this.bones) bone.setToSetupPose(); - for (const constraint of this.ikConstraints) constraint.setToSetupPose(); - for (const constraint of this.transformConstraints) constraint.setToSetupPose(); - for (const constraint of this.pathConstraints) constraint.setToSetupPose(); - for (const constraint of this.physicsConstraints) constraint.setToSetupPose(); + setupPoseBones () { + const bones = this.bones; + for (let i = 0, n = bones.length; i < n; i++) + bones[i].setupPose(); + + const constraints = this.constraints; + for (let i = 0, n = constraints.length; i < n; i++) + constraints[i].setupPose(); } /** Sets the slots and draw order to their setup pose values. */ - setSlotsToSetupPose () { + setupPoseSlots () { let slots = this.slots; Utils.arrayCopy(slots, 0, this.drawOrder, 0, slots.length); for (let i = 0, n = slots.length; i < n; i++) - slots[i].setToSetupPose(); + slots[i].setupPose(); } - /** @returns May return null. */ + /** Returns the root bone, or null if the skeleton has no bones. */ getRootBone () { if (this.bones.length == 0) return null; return this.bones[0]; } - /** @returns May be 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 + * repeatedly. */ findBone (boneName: string) { if (!boneName) throw new Error("boneName cannot be null."); let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - if (bone.data.name == boneName) return bone; - } + for (let i = 0, n = bones.length; i < n; i++) + if (bones[i].data.name == boneName) return bones[i]; return null; } /** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it - * repeatedly. - * @returns May be null. */ + * repeatedly. */ findSlot (slotName: string) { if (!slotName) throw new Error("slotName cannot be null."); let slots = this.slots; - for (let i = 0, n = slots.length; i < n; i++) { - let slot = slots[i]; - if (slot.data.name == slotName) return slot; - } + for (let i = 0, n = slots.length; i < n; i++) + if (slots[i].data.name == slotName) return slots[i]; return null; } /** Sets a skin by name. * - * See {@link #setSkin()}. */ - setSkinByName (skinName: string) { + * 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. + *

+ * 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 + * rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new skin. */ + setSkin (newSkin: Skin): void; + + setSkin (newSkin: Skin | string): void { + if (newSkin instanceof Skin) + this.setSkinBySkin(newSkin); + else + this.setSkinByName(newSkin); + }; + + private setSkinByName (skinName: string) { let skin = this.data.findSkin(skinName); if (!skin) throw new Error("Skin not found: " + skinName); this.setSkin(skin); } - /** Sets the skin used to look up attachments before looking in the {@link SkeletonData#defaultSkin default skin}. 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 #setSlotsToSetupPose()}. Also, often {@link AnimationState#apply()} 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. - * @param newSkin May be null. */ - setSkin (newSkin: Skin) { + private setSkinBySkin (newSkin: Skin) { if (newSkin == this.skin) return; if (newSkin) { if (this.skin) @@ -521,7 +349,7 @@ export class Skeleton { let name = slot.data.attachmentName; if (name) { let attachment = newSkin.getAttachment(i, name); - if (attachment) slot.setAttachment(attachment); + if (attachment) slot.pose.setAttachment(attachment); } } } @@ -530,13 +358,30 @@ export class Skeleton { this.updateCache(); } + /** Finds an attachment by looking in the {@link skin} and {@link SkeletonData.defaultSkin} using the slot name and attachment + * name. + * + * See {@link getAttachment(number, string)}. */ + getAttachment (slotName: string, attachmentName: 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 (slotNameOrIndex: string | number, attachmentName: string): Attachment | null { + if (typeof slotNameOrIndex === 'string') + return this.getAttachmentByName(slotNameOrIndex, attachmentName); + return this.getAttachmentByIndex(slotNameOrIndex, attachmentName); + } /** Finds an attachment by looking in the {@link #skin} and {@link SkeletonData#defaultSkin} using the slot name and attachment * name. * * See {@link #getAttachment()}. * @returns May be null. */ - getAttachmentByName (slotName: string, attachmentName: string): Attachment | null { + private getAttachmentByName (slotName: string, attachmentName: string): Attachment | null { let slot = this.data.findSlot(slotName); if (!slot) throw new Error(`Can't find slot with name ${slotName}`); return this.getAttachment(slot.index, attachmentName); @@ -547,7 +392,7 @@ export class Skeleton { * * See [Runtime skins](http://esotericsoftware.com/spine-runtime-skins) in the Spine Runtimes Guide. * @returns May be null. */ - getAttachment (slotIndex: number, attachmentName: string): Attachment | null { + private getAttachmentByIndex (slotIndex: number, attachmentName: string): Attachment | null { if (!attachmentName) throw new Error("attachmentName cannot be null."); if (this.skin) { let attachment = this.skin.getAttachment(slotIndex, attachmentName); @@ -557,57 +402,31 @@ export class Skeleton { return null; } - /** A convenience method to set an attachment by finding the slot with {@link #findSlot()}, finding the attachment with - * {@link #getAttachment()}, then setting the slot's {@link Slot#attachment}. + /** A convenience method to set an attachment by finding the slot with {@link findSlot()}, finding the attachment with + * {@link getAttachment()}, then setting the slot's {@link Slot.attachment}. * @param attachmentName May be null to clear the slot's attachment. */ setAttachment (slotName: string, attachmentName: string) { if (!slotName) throw new Error("slotName cannot be null."); - let slots = this.slots; - for (let i = 0, n = slots.length; i < n; i++) { - let slot = slots[i]; - if (slot.data.name == slotName) { - let attachment: Attachment | null = null; - if (attachmentName) { - attachment = this.getAttachment(i, attachmentName); - if (!attachment) throw new Error("Attachment not found: " + attachmentName + ", for slot: " + slotName); - } - slot.setAttachment(attachment); - return; - } + const slot = this.findSlot(slotName); + if (!slot) throw new Error("Slot not found: " + slotName); + let attachment: Attachment | null = null; + if (attachmentName) { + attachment = this.getAttachment(slot.data.index, attachmentName); + if (!attachment) + throw new Error("Attachment not found: " + attachmentName + ", for slot: " + slotName); } - throw new Error("Slot not found: " + slotName); + slot.pose.setAttachment(attachment); } - - /** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method - * than to call it repeatedly. - * @return May be null. */ - findIkConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - return this.ikConstraints.find((constraint) => constraint.data.name == constraintName) ?? null; - } - - /** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of - * this method than to call it repeatedly. - * @return May be null. */ - findTransformConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - return this.transformConstraints.find((constraint) => constraint.data.name == constraintName) ?? null; - } - - /** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method - * than to call it repeatedly. - * @return May be null. */ - findPathConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - return this.pathConstraints.find((constraint) => constraint.data.name == constraintName) ?? null; - } - - /** Finds a physics constraint by comparing each physics constraint's name. It is more efficient to cache the results of this - * method than to call it repeatedly. */ - findPhysicsConstraint (constraintName: string) { + findConstraint> (constraintName: string, type: new () => T): T | null { if (constraintName == null) throw new Error("constraintName cannot be null."); - return this.physicsConstraints.find((constraint) => constraint.data.name == constraintName) ?? null; + if (type == null) throw new Error("type cannot be null."); + const constraints = this.constraints; + for (let i = 0, n = constraints.length; i < n; i++) { + const constraint = constraints[i]; + if (constraint instanceof type && constraint.data.name === constraintName) return constraint as T; + } + 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 }`. @@ -635,71 +454,76 @@ export class Skeleton { let verticesLength = 0; let vertices: NumberArrayLike | null = null; let triangles: NumberArrayLike | null = null; - let attachment = slot.getAttachment(); - if (attachment instanceof RegionAttachment) { - verticesLength = 8; - vertices = Utils.setArraySize(temp, verticesLength, 0); - attachment.computeWorldVertices(slot, vertices, 0, 2); - triangles = Skeleton.quadTriangles; - } else if (attachment instanceof MeshAttachment) { - verticesLength = attachment.worldVerticesLength; - vertices = Utils.setArraySize(temp, verticesLength, 0); - attachment.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2); - triangles = attachment.triangles; - } else if (attachment instanceof ClippingAttachment && clipper != null) { - clipper.clipStart(slot, attachment); - continue; - } - if (vertices && triangles) { - if (clipper != null && clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length)) { - vertices = clipper.clippedVertices; - verticesLength = clipper.clippedVertices.length; + let attachment = slot.pose.attachment; + if (attachment) { + if (attachment instanceof RegionAttachment) { + verticesLength = 8; + vertices = Utils.setArraySize(temp, verticesLength, 0); + attachment.computeWorldVertices(slot, vertices, 0, 2); + triangles = Skeleton.quadTriangles; + } else if (attachment instanceof MeshAttachment) { + verticesLength = attachment.worldVerticesLength; + vertices = Utils.setArraySize(temp, verticesLength, 0); + attachment.computeWorldVertices(this, slot, 0, verticesLength, vertices, 0, 2); + triangles = attachment.triangles; + } else if (attachment instanceof ClippingAttachment && clipper) { + clipper.clipEnd(slot); + clipper.clipStart(this, slot, attachment); + continue; } - for (let ii = 0, nn = vertices.length; ii < nn; ii += 2) { - let x = vertices[ii], y = vertices[ii + 1]; - minX = Math.min(minX, x); - minY = Math.min(minY, y); - maxX = Math.max(maxX, x); - maxY = Math.max(maxY, y); + if (vertices && triangles) { + if (clipper && clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length)) { + vertices = clipper.clippedVertices; + verticesLength = clipper.clippedVertices.length; + } + for (let ii = 0, nn = vertices.length; ii < nn; ii += 2) { + let x = vertices[ii], y = vertices[ii + 1]; + minX = Math.min(minX, x); + minY = Math.min(minY, y); + maxX = Math.max(maxX, x); + maxY = Math.max(maxY, y); + } } } - if (clipper != null) clipper.clipEndWithSlot(slot); + if (clipper) clipper.clipEnd(slot); } - if (clipper != null) clipper.clipEnd(); + if (clipper) clipper.clipEnd(); offset.set(minX, minY); size.set(maxX - minX, maxY - minY); } + /** Scales the entire skeleton on the X and Y axes. + * + * Bones that do not inherit scale are still affected by this property. */ + public setScale (scaleX: number, scaleY: number) { + this.scaleX = scaleX; + this.scaleY = scaleY; + } + + /** Sets the skeleton X and Y position, which is added to the root bone worldX and worldY position. + * + * Bones that do not inherit translation are still affected by this property. */ + public setPosition (x: number, y: number) { + this.x = x; + this.y = y; + } + /** Increments the skeleton's {@link #time}. */ update (delta: number) { this.time += delta; } + /** Calls {@link PhysicsConstraint.translate} for each physics constraint. */ physicsTranslate (x: number, y: number) { - const physicsConstraints = this.physicsConstraints; - for (let i = 0, n = physicsConstraints.length; i < n; i++) - physicsConstraints[i].translate(x, y); + const constraints = this.physics; + for (let i = 0, n = constraints.length; i < n; i++) + constraints[i].translate(x, y); } - /** Calls {@link PhysicsConstraint#rotate(float, float, float)} for each physics constraint. */ + /** Calls {@link PhysicsConstraint.rotate} for each physics constraint. */ physicsRotate (x: number, y: number, degrees: number) { - const physicsConstraints = this.physicsConstraints; - for (let i = 0, n = physicsConstraints.length; i < n; i++) - physicsConstraints[i].rotate(x, y, degrees); + const constraints = this.physics; + for (let i = 0, n = constraints.length; i < n; i++) + constraints[i].rotate(x, y, degrees); } } - -/** Determines how physics and other non-deterministic updates are applied. */ -export enum Physics { - /** Physics are not updated or applied. */ - none, - - /** Physics are reset to the current pose. */ - reset, - - /** Physics are updated and the pose from physics is applied. */ - update, - - /** Physics are not updated but the pose from physics is applied. */ - pose -} diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts index 31444e657..c804ed877 100644 --- a/spine-ts/spine-core/src/SkeletonBinary.ts +++ b/spine-ts/spine-core/src/SkeletonBinary.ts @@ -27,7 +27,7 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Animation, Timeline, InheritTimeline, AttachmentTimeline, RGBATimeline, RGBTimeline, RGBA2Timeline, RGB2Timeline, AlphaTimeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline2, CurveTimeline, SequenceTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintWindTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintMixTimeline } from "./Animation.js"; +import { Animation, Timeline, InheritTimeline, AttachmentTimeline, RGBATimeline, RGBTimeline, RGBA2Timeline, RGB2Timeline, AlphaTimeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline, SequenceTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintWindTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintMixTimeline, BoneTimeline2, SliderTimeline, SliderMixTimeline } from "./Animation.js"; import { VertexAttachment, Attachment } from "./attachments/Attachment.js"; import { AttachmentLoader } from "./attachments/AttachmentLoader.js"; import { HasTextureRegion } from "./attachments/HasTextureRegion.js"; @@ -37,10 +37,12 @@ import { BoneData } from "./BoneData.js"; import { Event } from "./Event.js"; import { EventData } from "./EventData.js"; import { IkConstraintData } from "./IkConstraintData.js"; +import { PathConstraint } from "./PathConstraint.js"; import { PathConstraintData, PositionMode, SpacingMode } from "./PathConstraintData.js"; import { PhysicsConstraintData } from "./PhysicsConstraintData.js"; import { SkeletonData } from "./SkeletonData.js"; import { Skin } from "./Skin.js"; +import { SliderData } from "./SliderData.js"; import { SlotData } from "./SlotData.js"; import { FromProperty, FromRotate, FromScaleX, FromScaleY, FromShearY, FromX, FromY, ToProperty, ToRotate, ToScaleX, ToScaleY, ToShearY, ToX, ToY, TransformConstraintData } from "./TransformConstraintData.js"; import { Color, Utils } from "./Utils.js"; @@ -99,28 +101,30 @@ export class SkeletonBinary { } // Bones. + const bones = skeletonData.bones; n = input.readInt(true) for (let i = 0; i < n; i++) { let name = input.readString(); if (!name) throw new Error("Bone name must not be null."); - let parent = i == 0 ? null : skeletonData.bones[input.readInt(true)]; + let parent = i == 0 ? null : bones[input.readInt(true)]; let data = new BoneData(i, name, parent); - data.rotation = input.readFloat(); - data.x = input.readFloat() * scale; - data.y = input.readFloat() * scale; - data.scaleX = input.readFloat(); - data.scaleY = input.readFloat(); - data.shearX = input.readFloat(); - data.shearY = input.readFloat(); + const setup = data.setup; + setup.rotation = input.readFloat(); + setup.x = input.readFloat() * scale; + setup.y = input.readFloat() * scale; + setup.scaleX = input.readFloat(); + setup.scaleY = input.readFloat(); + setup.shearX = input.readFloat(); + setup.shearY = input.readFloat(); + setup.inherit = input.readByte(); data.length = input.readFloat() * scale; - data.inherit = input.readByte(); data.skinRequired = input.readBoolean(); if (nonessential) { Color.rgba8888ToColor(data.color, input.readInt32()); data.icon = input.readString() ?? undefined; data.visible = input.readBoolean(); } - skeletonData.bones.push(data); + bones.push(data); } // Slots. @@ -128,12 +132,12 @@ export class SkeletonBinary { for (let i = 0; i < n; i++) { let slotName = input.readString(); if (!slotName) throw new Error("Slot name must not be null."); - let boneData = skeletonData.bones[input.readInt(true)]; + let boneData = bones[input.readInt(true)]; let data = new SlotData(i, slotName, boneData); - Color.rgba8888ToColor(data.color, input.readInt32()); + Color.rgba8888ToColor(data.setup.color, input.readInt32()); let darkColor = input.readInt32(); - if (darkColor != -1) Color.rgb888ToColor(data.darkColor = new Color(), darkColor); + if (darkColor != -1) Color.rgb888ToColor(data.setup.darkColor = new Color(), darkColor); data.attachmentName = input.readStringRef(); data.blendMode = input.readInt(true); @@ -141,156 +145,203 @@ export class SkeletonBinary { skeletonData.slots.push(data); } - // IK constraints. - n = input.readInt(true); - for (let i = 0, nn; i < n; i++) { + // Constraints. + const constraints = skeletonData.constraints; + const constraintCount = input.readInt(true); + for (let i = 0; i < constraintCount; i++) { let name = input.readString(); - if (!name) throw new Error("IK constraint data name must not be null."); - let data = new IkConstraintData(name); - data.order = input.readInt(true); - nn = input.readInt(true); - for (let ii = 0; ii < nn; ii++) - data.bones.push(skeletonData.bones[input.readInt(true)]); - data.target = skeletonData.bones[input.readInt(true)]; - let flags = input.readByte(); - data.skinRequired = (flags & 1) != 0; - data.bendDirection = (flags & 2) != 0 ? 1 : -1; - data.compress = (flags & 4) != 0; - data.stretch = (flags & 8) != 0; - data.uniform = (flags & 16) != 0; - if ((flags & 32) != 0) data.mix = (flags & 64) != 0 ? input.readFloat() : 1; - if ((flags & 128) != 0) data.softness = input.readFloat() * scale; - skeletonData.ikConstraints.push(data); - } - - // Transform constraints. - n = input.readInt(true); - for (let i = 0, nn; i < n; i++) { - let name = input.readString(); - if (!name) throw new Error("Transform constraint data name must not be null."); - let data = new TransformConstraintData(name); - data.order = input.readInt(true); - nn = input.readInt(true); - for (let ii = 0; ii < nn; ii++) - data.bones.push(skeletonData.bones[input.readInt(true)]); - data.source = skeletonData.bones[input.readInt(true)]; - let flags = input.readUnsignedByte(); - data.skinRequired = (flags & 1) != 0; - data.localSource = (flags & 2) != 0; - data.localTarget = (flags & 4) != 0; - data.additive = (flags & 8) != 0; - data.clamp = (flags & 16) != 0; - - nn = flags >> 5; - for (let ii = 0, tn; ii < nn; ii++) { - let from: FromProperty | null; - let type = input.readByte(); - switch (type) { - case 0: from = new FromRotate(); break; - case 1: from = new FromX(); break; - case 2: from = new FromY(); break; - case 3: from = new FromScaleX(); break; - case 4: from = new FromScaleY(); break; - case 5: from = new FromShearY(); break; - default: from = null; + if (!name) throw new Error("Constraint data name must not be null."); + let nn = input.readInt(true); + switch (input.readByte()) { + case CONSTRAINT_IK: { + let data = new IkConstraintData(name); + for (let ii = 0; ii < nn; ii++) + data.bones.push(bones[input.readInt(true)]); + data.target = bones[input.readInt(true)]; + let flags = input.readByte(); + data.skinRequired = (flags & 1) != 0; + data.uniform = (flags & 2) != 0; + const setup = data.setup; + setup.bendDirection = (flags & 4) != 0 ? 1 : -1; + setup.compress = (flags & 8) != 0; + setup.stretch = (flags & 16) != 0; + if ((flags & 32) != 0) setup.mix = (flags & 64) != 0 ? input.readFloat() : 1; + if ((flags & 128) != 0) setup.softness = input.readFloat() * scale; + constraints.push(data); + break; } - if (!from) continue; - from.offset = input.readFloat() * scale; - tn = input.readByte(); - for (let t = 0; t < tn; t++) { - let to: ToProperty | null; - type = input.readByte(); - switch (type) { - case 0: to = new ToRotate(); break; - case 1: to = new ToX(); break; - case 2: to = new ToY(); break; - case 3: to = new ToScaleX(); break; - case 4: to = new ToScaleY(); break; - case 5: to = new ToShearY(); break; - default: to = null; + case CONSTRAINT_TRANSFORM: { + let data = new TransformConstraintData(name); + for (let ii = 0; ii < nn; ii++) + data.bones.push(bones[input.readInt(true)]); + data.source = bones[input.readInt(true)]; + let flags = input.readUnsignedByte(); + data.skinRequired = (flags & 1) != 0; + data.localSource = (flags & 2) != 0; + data.localTarget = (flags & 4) != 0; + data.additive = (flags & 8) != 0; + data.clamp = (flags & 16) != 0; + + nn = flags >> 5; + for (let ii = 0, tn; ii < nn; ii++) { + let fromScale = 1; + let from: FromProperty | null; + switch (input.readByte()) { + case 0: from = new FromRotate(); break; + case 1: { + fromScale = scale; + from = new FromX(); + break; + } + case 2: { + fromScale = scale; + from = new FromY(); + break; + } + case 3: from = new FromScaleX(); break; + case 4: from = new FromScaleY(); break; + case 5: from = new FromShearY(); break; + default: from = null; + } + if (!from) continue; + from.offset = input.readFloat() * fromScale; + tn = input.readByte(); + for (let t = 0; t < tn; t++) { + let toScale = 1; + let to: ToProperty | null; + switch (input.readByte()) { + case 0: to = new ToRotate(); break; + case 1: { + toScale = scale; + to = new ToX(); + break; + } + case 2: { + toScale = scale; + to = new ToY(); + break; + } + case 3: to = new ToScaleX(); break; + case 4: to = new ToScaleY(); break; + case 5: to = new ToShearY(); break; + default: to = null; + } + if (!to) continue; + to.offset = input.readFloat() * scale; + to.max = input.readFloat() * scale; + to.scale = input.readFloat(); + from.to[t] = to; + } + data.properties[ii] = from; } - if (!to) continue; - to.offset = input.readFloat() * scale; - to.max = input.readFloat() * scale; - to.scale = input.readFloat(); - from.to[t] = to; + flags = input.readByte(); + if ((flags & 1) != 0) data.offsets[0] = input.readFloat(); + if ((flags & 2) != 0) data.offsets[1] = input.readFloat() * scale; + if ((flags & 4) != 0) data.offsets[2] = input.readFloat() * scale; + if ((flags & 8) != 0) data.offsets[3] = input.readFloat(); + if ((flags & 16) != 0) data.offsets[4] = input.readFloat(); + if ((flags & 32) != 0) data.offsets[5] = input.readFloat(); + flags = input.readByte(); + const setup = data.setup; + if ((flags & 1) != 0) setup.mixRotate = input.readFloat(); + if ((flags & 2) != 0) setup.mixX = input.readFloat(); + if ((flags & 4) != 0) setup.mixY = input.readFloat(); + if ((flags & 8) != 0) setup.mixScaleX = input.readFloat(); + if ((flags & 16) != 0) setup.mixScaleY = input.readFloat(); + if ((flags & 32) != 0) setup.mixShearY = input.readFloat(); + constraints.push(data); + break; + } + case CONSTRAINT_PATH: { + let data = new PathConstraintData(name); + data.skinRequired = input.readBoolean(); + nn = input.readInt(true); + for (let ii = 0; ii < nn; ii++) + data.bones.push(bones[input.readInt(true)]); + data.slot = skeletonData.slots[input.readInt(true)]; + const flags = input.readByte(); + data.skinRequired = (flags & 1) != 0; + data.positionMode = (flags >> 1) & 2; + data.spacingMode = (flags >> 2) & 3; + data.rotateMode = (flags >> 4) & 3; + if ((flags & 128) != 0) data.offsetRotation = input.readFloat(); + const setup = data.setup; + setup.position = input.readFloat(); + if (data.positionMode == PositionMode.Fixed) setup.position *= scale; + setup.spacing = input.readFloat(); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) setup.spacing *= scale; + setup.mixRotate = input.readFloat(); + setup.mixX = input.readFloat(); + setup.mixY = input.readFloat(); + constraints.push(data); + break; + } + case CONSTRAINT_PHYSICS: { + const data = new PhysicsConstraintData(name); + data.bone = bones[nn]; + let flags = input.readByte(); + data.skinRequired = (flags & 1) != 0; + if ((flags & 2) != 0) data.x = input.readFloat(); + if ((flags & 4) != 0) data.y = input.readFloat(); + if ((flags & 8) != 0) data.rotate = input.readFloat(); + if ((flags & 16) != 0) data.scaleX = input.readFloat(); + 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; + setup.inertia = input.readFloat(); + setup.strength = input.readFloat(); + setup.damping = input.readFloat(); + setup.massInverse = (flags & 128) != 0 ? input.readFloat() : 1; + setup.wind = input.readFloat(); + setup.gravity = input.readFloat(); + flags = input.readByte(); + if ((flags & 1) != 0) data.inertiaGlobal = true; + if ((flags & 2) != 0) data.strengthGlobal = true; + if ((flags & 4) != 0) data.dampingGlobal = true; + if ((flags & 8) != 0) data.massGlobal = true; + if ((flags & 16) != 0) data.windGlobal = true; + if ((flags & 32) != 0) data.gravityGlobal = true; + if ((flags & 64) != 0) data.mixGlobal = true; + setup.mix = (flags & 128) != 0 ? input.readFloat() : 1; + constraints.push(data); + break; + } + case CONSTRAINT_PHYSICS: { + const data = new SliderData(name); + data.skinRequired = (nn & 1) != 0; + data.loop = (nn & 2) != 0; + data.additive = (nn & 4) != 0; + if ((nn & 8) != 0) data.setup.time = input.readFloat(); + if ((nn & 16) != 0) data.setup.mix = (nn & 32) != 0 ? input.readFloat() : 1; + if ((nn & 64) != 0) { + data.local = (nn & 128) != 0; + data.bone = bones[input.readInt(true)]; + let offset = input.readFloat(); + switch (input.readByte()) { + case 0: data.property = new FromRotate(); break; + case 1: { + offset *= scale; + data.property = new FromX(); + break; + } + case 2: { + offset *= scale; + data.property = new FromY(); + break; + } + case 3: data.property = new FromScaleX(); break; + case 4: data.property = new FromScaleY(); break; + case 5: data.property = new FromShearY(); break; + default: continue; + }; + data.property.offset = offset; + data.scale = input.readFloat(); + } + constraints.push(data); + break; } - data.properties[ii] = from; } - - flags = input.readByte(); - if ((flags & 1) != 0) data.offsetX = input.readFloat(); - if ((flags & 2) != 0) data.offsetY = input.readFloat(); - if ((flags & 4) != 0) data.mixRotate = input.readFloat(); - if ((flags & 8) != 0) data.mixX = input.readFloat(); - if ((flags & 16) != 0) data.mixY = input.readFloat(); - if ((flags & 32) != 0) data.mixScaleX = input.readFloat(); - if ((flags & 64) != 0) data.mixScaleY = input.readFloat(); - if ((flags & 128) != 0) data.mixShearY = input.readFloat(); - - skeletonData.transformConstraints.push(data); - } - - // Path constraints. - n = input.readInt(true); - for (let i = 0, nn; i < n; i++) { - let name = input.readString(); - if (!name) throw new Error("Path constraint data name must not be null."); - let data = new PathConstraintData(name); - data.order = input.readInt(true); - data.skinRequired = input.readBoolean(); - nn = input.readInt(true); - for (let ii = 0; ii < nn; ii++) - data.bones.push(skeletonData.bones[input.readInt(true)]); - data.slot = skeletonData.slots[input.readInt(true)]; - const flags = input.readByte(); - data.positionMode = flags & 1; - data.spacingMode = (flags >> 1) & 3; - data.rotateMode = (flags >> 3) & 3; - if ((flags & 128) != 0) data.offsetRotation = input.readFloat(); - data.position = input.readFloat(); - if (data.positionMode == PositionMode.Fixed) data.position *= scale; - data.spacing = input.readFloat(); - if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; - data.mixRotate = input.readFloat(); - data.mixX = input.readFloat(); - data.mixY = input.readFloat(); - skeletonData.pathConstraints.push(data); - } - - // Physics constraints. - n = input.readInt(true); - for (let i = 0, nn; i < n; i++) { - const name = input.readString(); - if (!name) throw new Error("Physics constraint data name must not be null."); - const data = new PhysicsConstraintData(name); - data.order = input.readInt(true); - data.bone = skeletonData.bones[input.readInt(true)]; - let flags = input.readByte(); - data.skinRequired = (flags & 1) != 0; - if ((flags & 2) != 0) data.x = input.readFloat(); - if ((flags & 4) != 0) data.y = input.readFloat(); - if ((flags & 8) != 0) data.rotate = input.readFloat(); - if ((flags & 16) != 0) data.scaleX = input.readFloat(); - if ((flags & 32) != 0) data.shearX = input.readFloat(); - data.limit = ((flags & 64) != 0 ? input.readFloat() : 5000) * scale; - data.step = 1 / input.readUnsignedByte(); - data.inertia = input.readFloat(); - data.strength = input.readFloat(); - data.damping = input.readFloat(); - data.massInverse = (flags & 128) != 0 ? input.readFloat() : 1; - data.wind = input.readFloat(); - data.gravity = input.readFloat(); - flags = input.readByte(); - if ((flags & 1) != 0) data.inertiaGlobal = true; - if ((flags & 2) != 0) data.strengthGlobal = true; - if ((flags & 4) != 0) data.dampingGlobal = true; - if ((flags & 8) != 0) data.massGlobal = true; - if ((flags & 16) != 0) data.windGlobal = true; - if ((flags & 32) != 0) data.gravityGlobal = true; - if ((flags & 64) != 0) data.mixGlobal = true; - data.mix = (flags & 128) != 0 ? input.readFloat() : 1; - skeletonData.physicsConstraints.push(data); } // Default skin. @@ -343,12 +394,19 @@ export class SkeletonBinary { } // Animations. + const animations = skeletonData.animations; n = input.readInt(true); for (let i = 0; i < n; i++) { let animationName = input.readString(); - if (!animationName) throw new Error("Animatio name must not be null."); - skeletonData.animations.push(this.readAnimation(input, animationName, skeletonData)); + if (!animationName) throw new Error("Animation name must not be null."); + animations.push(this.readAnimation(input, animationName, skeletonData)); } + + for (let i = 0; i < constraintCount; i++) { + const constraint = constraints[i]; + if (constraint instanceof SliderData) constraint.animation = animations[input.readInt(true)]; + } + return skeletonData; } @@ -364,19 +422,19 @@ export class SkeletonBinary { let skinName = input.readString(); if (!skinName) throw new Error("Skin name must not be null."); skin = new Skin(skinName); - if (nonessential) Color.rgba8888ToColor(skin.color, input.readInt32()); - skin.bones.length = input.readInt(true); - for (let i = 0, n = skin.bones.length; i < n; i++) - skin.bones[i] = skeletonData.bones[input.readInt(true)]; - for (let i = 0, n = input.readInt(true); i < n; i++) - skin.constraints.push(skeletonData.ikConstraints[input.readInt(true)]); - for (let i = 0, n = input.readInt(true); i < n; i++) - skin.constraints.push(skeletonData.transformConstraints[input.readInt(true)]); - for (let i = 0, n = input.readInt(true); i < n; i++) - skin.constraints.push(skeletonData.pathConstraints[input.readInt(true)]); - for (let i = 0, n = input.readInt(true); i < n; i++) - skin.constraints.push(skeletonData.physicsConstraints[input.readInt(true)]); + if (nonessential) Color.rgba8888ToColor(skin.color, input.readInt32()); + + let n = input.readInt(true); + let from: Object[] = skeletonData.bones, to: Object[] = skin.bones; + for (let i = 0; i < n; i++) + to[i] = from[input.readInt(true)]; + + n = input.readInt(true); + from = skeletonData.constraints; + to = skin.constraints; + for (let i = 0; i < n; i++) + to[i] = from[input.readInt(true)]; slotCount = input.readInt(true); } @@ -553,7 +611,6 @@ export class SkeletonBinary { return clip; } } - return null; } private readSequence (input: BinaryInput) { @@ -838,35 +895,16 @@ export class SkeletonBinary { } let bezierCount = input.readInt(true); switch (type) { - case BONE_ROTATE: - timelines.push(readTimeline1(input, new RotateTimeline(frameCount, bezierCount, boneIndex), 1)); - break; - case BONE_TRANSLATE: - timelines.push(readTimeline2(input, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale)); - break; - case BONE_TRANSLATEX: - timelines.push(readTimeline1(input, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale)); - break; - case BONE_TRANSLATEY: - timelines.push(readTimeline1(input, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale)); - break; - case BONE_SCALE: - timelines.push(readTimeline2(input, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1)); - break; - case BONE_SCALEX: - timelines.push(readTimeline1(input, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1)); - break; - case BONE_SCALEY: - timelines.push(readTimeline1(input, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1)); - break; - case BONE_SHEAR: - timelines.push(readTimeline2(input, new ShearTimeline(frameCount, bezierCount, boneIndex), 1)); - break; - case BONE_SHEARX: - timelines.push(readTimeline1(input, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1)); - break; - case BONE_SHEARY: - timelines.push(readTimeline1(input, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1)); + case BONE_ROTATE: readTimeline(input, timelines, new RotateTimeline(frameCount, bezierCount, boneIndex), 1); break; + case BONE_TRANSLATE: readTimeline(input, timelines, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale); break; + case BONE_TRANSLATEX: readTimeline(input, timelines, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale); break; + case BONE_TRANSLATEY: readTimeline(input, timelines, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale); break; + case BONE_SCALE: readTimeline(input, timelines, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1); break; + case BONE_SCALEX: readTimeline(input, timelines, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1); break; + case BONE_SCALEY: readTimeline(input, timelines, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1); break; + case BONE_SHEAR: readTimeline(input, timelines, new ShearTimeline(frameCount, bezierCount, boneIndex), 1); break; + case BONE_SHEARX: readTimeline(input, timelines, new ShearXTimeline(frameCount, bezierCount, boneIndex), 1); break; + case BONE_SHEARY: readTimeline(input, timelines, new ShearYTimeline(frameCount, bezierCount, boneIndex), 1); break; } } } @@ -934,19 +972,17 @@ export class SkeletonBinary { // Path constraint timelines. for (let i = 0, n = input.readInt(true); i < n; i++) { let index = input.readInt(true); - let data = skeletonData.pathConstraints[index]; + let data = skeletonData.constraints[index] as PathConstraintData; for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { const type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true); switch (type) { case PATH_POSITION: - timelines - .push(readTimeline1(input, new PathConstraintPositionTimeline(frameCount, bezierCount, index), - data.positionMode == PositionMode.Fixed ? scale : 1)); + readTimeline(input, timelines, new PathConstraintPositionTimeline(frameCount, bezierCount, index), + data.positionMode == PositionMode.Fixed ? scale : 1); break; case PATH_SPACING: - timelines - .push(readTimeline1(input, new PathConstraintSpacingTimeline(frameCount, bezierCount, index), - data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1)); + readTimeline(input, timelines, new PathConstraintSpacingTimeline(frameCount, bezierCount, index), + data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed ? scale : 1); break; case PATH_MIX: let timeline = new PathConstraintMixTimeline(frameCount, bezierCount, index); @@ -989,33 +1025,33 @@ export class SkeletonBinary { } const bezierCount = input.readInt(true); switch (type) { - case PHYSICS_INERTIA: - timelines.push(readTimeline1(input, new PhysicsConstraintInertiaTimeline(frameCount, bezierCount, index), 1)); - break; - case PHYSICS_STRENGTH: - timelines.push(readTimeline1(input, new PhysicsConstraintStrengthTimeline(frameCount, bezierCount, index), 1)); - break; - case PHYSICS_DAMPING: - timelines.push(readTimeline1(input, new PhysicsConstraintDampingTimeline(frameCount, bezierCount, index), 1)); - break; - case PHYSICS_MASS: - timelines.push(readTimeline1(input, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1)); - break; - case PHYSICS_WIND: - timelines.push(readTimeline1(input, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1)); - break; - case PHYSICS_GRAVITY: - timelines.push(readTimeline1(input, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1)); - break; - case PHYSICS_MIX: - timelines.push(readTimeline1(input, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1)); - default: - throw new Error("Unknown physics timeline type."); + case PHYSICS_INERTIA: readTimeline(input, timelines, new PhysicsConstraintInertiaTimeline(frameCount, bezierCount, index), 1); break; + case PHYSICS_STRENGTH: readTimeline(input, timelines, new PhysicsConstraintStrengthTimeline(frameCount, bezierCount, index), 1); break; + case PHYSICS_DAMPING: readTimeline(input, timelines, new PhysicsConstraintDampingTimeline(frameCount, bezierCount, index), 1); break; + case PHYSICS_MASS: readTimeline(input, timelines, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1); break; + case PHYSICS_WIND: readTimeline(input, timelines, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1); break; + case PHYSICS_GRAVITY: readTimeline(input, timelines, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1); break; + case PHYSICS_MIX: readTimeline(input, timelines, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1); break; + default: throw new Error("Unknown physics timeline type."); } } } - // Deform timelines. + // Slider timelines. + for (let i = 0, n = input.readInt(true); i < n; i++) { + const index = input.readInt(true); + for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { + const type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true); + switch (type) { + case SLIDER_TIME: readTimeline(input, timelines, new SliderTimeline(frameCount, bezierCount, index), 1); break; + case SLIDER_MIX: readTimeline(input, timelines, new SliderMixTimeline(frameCount, bezierCount, index), 1); break; + default: throw new Error("Uknown slider type: " + type); + } + + } + } + + // Attachment timelines. for (let i = 0, n = input.readInt(true); i < n; i++) { let skin = skeletonData.skins[input.readInt(true)]; for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { @@ -1267,7 +1303,16 @@ class Vertices { enum AttachmentType { Region, BoundingBox, Mesh, LinkedMesh, Path, Point, Clipping } -function readTimeline1 (input: BinaryInput, timeline: CurveTimeline1, scale: number): CurveTimeline1 { +function readTimeline (input: BinaryInput, timelines: Array, timeline: CurveTimeline1, scale: number): void; +function readTimeline (input: BinaryInput, timelines: Array, timeline: BoneTimeline2, scale: number): void; +function readTimeline (input: BinaryInput, timelines: Array, timeline: CurveTimeline1 | BoneTimeline2, scale: number): void { + if (timeline instanceof CurveTimeline1) + readTimeline1(input, timelines, timeline, scale); + else + readTimeline2(input, timelines, timeline, scale); +} + +function readTimeline1 (input: BinaryInput, timelines: Array, timeline: CurveTimeline1, scale: number): void { let time = input.readFloat(), value = input.readFloat() * scale; for (let frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1; ; frame++) { timeline.setFrame(frame, time, value); @@ -1283,10 +1328,10 @@ function readTimeline1 (input: BinaryInput, timeline: CurveTimeline1, scale: num time = time2; value = value2; } - return timeline; + timelines.push(timeline); } -function readTimeline2 (input: BinaryInput, timeline: CurveTimeline2, scale: number): CurveTimeline2 { +function readTimeline2 (input: BinaryInput, timelines: Array, timeline: BoneTimeline2, scale: number): void { let time = input.readFloat(), value1 = input.readFloat() * scale, value2 = input.readFloat() * scale; for (let frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1; ; frame++) { timeline.setFrame(frame, time, value1, value2); @@ -1304,7 +1349,7 @@ function readTimeline2 (input: BinaryInput, timeline: CurveTimeline2, scale: num value1 = nvalue1; value2 = nvalue2; } - return timeline; + timelines.push(timeline); } function setBezier (input: BinaryInput, timeline: CurveTimeline, bezier: number, frame: number, value: number, @@ -1331,6 +1376,12 @@ const SLOT_RGBA2 = 3; const SLOT_RGB2 = 4; const SLOT_ALPHA = 5; +const CONSTRAINT_IK = 0; +const CONSTRAINT_PATH = 1; +const CONSTRAINT_TRANSFORM = 2; +const CONSTRAINT_PHYSICS = 3; +const CONSTRAINT_SLIDER = 4; + const ATTACHMENT_DEFORM = 0; const ATTACHMENT_SEQUENCE = 1; @@ -1347,6 +1398,9 @@ const PHYSICS_GRAVITY = 6; const PHYSICS_MIX = 7; const PHYSICS_RESET = 8; +const SLIDER_TIME = 0; +const SLIDER_MIX = 1; + const CURVE_LINEAR = 0; const CURVE_STEPPED = 1; const CURVE_BEZIER = 2; diff --git a/spine-ts/spine-core/src/SkeletonBounds.ts b/spine-ts/spine-core/src/SkeletonBounds.ts index ac9d3eeb7..68cbc2b24 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++) { let slot = slots[i]; if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); + let attachment = slot.pose.attachment; if (attachment instanceof BoundingBoxAttachment) { boundingBoxes.push(attachment); @@ -85,7 +85,7 @@ export class SkeletonBounds { polygon = Utils.newFloatArray(attachment.worldVerticesLength); } polygons.push(polygon); - attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, polygon, 0, 2); + attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, polygon, 0, 2); } } diff --git a/spine-ts/spine-core/src/SkeletonClipping.ts b/spine-ts/spine-core/src/SkeletonClipping.ts index 2799ae6af..55d64adc5 100644 --- a/spine-ts/spine-core/src/SkeletonClipping.ts +++ b/spine-ts/spine-core/src/SkeletonClipping.ts @@ -28,6 +28,7 @@ *****************************************************************************/ import { ClippingAttachment } from "./attachments/ClippingAttachment.js"; +import { Skeleton } from "./Skeleton.js"; import { Slot } from "./Slot.js"; import { Triangulator } from "./Triangulator.js"; import { Utils, Color, NumberArrayLike } from "./Utils.js"; @@ -37,20 +38,23 @@ export class SkeletonClipping { private clippingPolygon = new Array(); private clipOutput = new Array(); clippedVertices = new Array(); + + /** An empty array unless {@link clipTrianglesUnpacked} was used. **/ clippedUVs = new Array(); + clippedTriangles = new Array(); private scratch = new Array(); private clipAttachment: ClippingAttachment | null = null; private clippingPolygons: Array> | null = null; - clipStart (slot: Slot, clip: ClippingAttachment): number { + clipStart (skeleton: Skeleton, slot: Slot, clip: ClippingAttachment): number { if (this.clipAttachment) return 0; this.clipAttachment = clip; let n = clip.worldVerticesLength; let vertices = Utils.setArraySize(this.clippingPolygon, n); - clip.computeWorldVertices(slot, 0, n, vertices, 0, 2); + clip.computeWorldVertices(skeleton, slot, 0, n, vertices, 0, 2); let clippingPolygon = this.clippingPolygon; SkeletonClipping.makeClockwise(clippingPolygon); let clippingPolygons = this.clippingPolygons = this.triangulator.decompose(clippingPolygon, this.triangulator.triangulate(clippingPolygon)); @@ -64,12 +68,9 @@ export class SkeletonClipping { return clippingPolygons.length; } - clipEndWithSlot (slot: Slot) { - if (this.clipAttachment && this.clipAttachment.endSlot == slot.data) this.clipEnd(); - } - - clipEnd () { + clipEnd (slot?: Slot) { if (!this.clipAttachment) return; + if (slot && this.clipAttachment.endSlot !== slot.data) return; this.clipAttachment = null; this.clippingPolygons = null; this.clippedVertices.length = 0; diff --git a/spine-ts/spine-core/src/SkeletonData.ts b/spine-ts/spine-core/src/SkeletonData.ts index 0bd6ce1ee..8b8649b3b 100644 --- a/spine-ts/spine-core/src/SkeletonData.ts +++ b/spine-ts/spine-core/src/SkeletonData.ts @@ -29,13 +29,10 @@ import { Animation } from "./Animation" import { BoneData } from "./BoneData.js"; +import { ConstraintData } from "./ConstraintData"; import { EventData } from "./EventData.js"; -import { IkConstraintData } from "./IkConstraintData.js"; -import { PathConstraintData } from "./PathConstraintData.js"; -import { PhysicsConstraintData } from "./PhysicsConstraintData.js"; import { Skin } from "./Skin.js"; import { SlotData } from "./SlotData.js"; -import { TransformConstraintData } from "./TransformConstraintData.js"; /** Stores the setup pose and all of the stateless data for a skeleton. * @@ -67,16 +64,7 @@ export class SkeletonData { animations = new Array(); /** The skeleton's IK constraints. */ - ikConstraints = new Array(); - - /** The skeleton's transform constraints. */ - transformConstraints = new Array(); - - /** The skeleton's path constraints. */ - pathConstraints = new Array(); - - /** The skeleton's physics constraints. */ - physicsConstraints = new Array(); + constraints = new Array>(); /** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */ x: number = 0; @@ -116,10 +104,8 @@ export class SkeletonData { findBone (boneName: string) { if (!boneName) throw new Error("boneName cannot be null."); let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - if (bone.name == boneName) return bone; - } + for (let i = 0, n = bones.length; i < n; i++) + if (bones[i].name == boneName) return bones[i]; return null; } @@ -129,10 +115,8 @@ export class SkeletonData { findSlot (slotName: string) { if (!slotName) throw new Error("slotName cannot be null."); let slots = this.slots; - for (let i = 0, n = slots.length; i < n; i++) { - let slot = slots[i]; - if (slot.name == slotName) return slot; - } + for (let i = 0, n = slots.length; i < n; i++) + if (slots[i].name == slotName) return slots[i]; return null; } @@ -142,10 +126,8 @@ export class SkeletonData { findSkin (skinName: string) { if (!skinName) throw new Error("skinName cannot be null."); let skins = this.skins; - for (let i = 0, n = skins.length; i < n; i++) { - let skin = skins[i]; - if (skin.name == skinName) return skin; - } + for (let i = 0, n = skins.length; i < n; i++) + if (skins[i].name == skinName) return skins[i]; return null; } @@ -155,10 +137,8 @@ export class SkeletonData { findEvent (eventDataName: string) { if (!eventDataName) throw new Error("eventDataName cannot be null."); let events = this.events; - for (let i = 0, n = events.length; i < n; i++) { - let event = events[i]; - if (event.name == eventDataName) return event; - } + for (let i = 0, n = events.length; i < n; i++) + if (events[i].name == eventDataName) return events[i]; return null; } @@ -168,62 +148,22 @@ export class SkeletonData { findAnimation (animationName: string) { if (!animationName) throw new Error("animationName cannot be null."); let animations = this.animations; - for (let i = 0, n = animations.length; i < n; i++) { - let animation = animations[i]; - if (animation.name == animationName) return animation; + for (let i = 0, n = animations.length; i < n; i++) + if (animations[i].name == animationName) return animations[i]; + return null; + } + + // --- Constraints. + + findConstraint> (constraintName: string, type: new (name: string) => T): T | null { + if (!constraintName) throw new Error("constraintName cannot be null."); + if (type == null) throw new Error("type cannot be null."); + const constraints = this.constraints; + for (let i = 0, n = this.constraints.length; i < n; i++) { + let constraint = constraints[i]; + if (constraint instanceof type && constraint.name === constraintName) return constraint as T; } return null; } - /** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method - * than to call it multiple times. - * @return May be null. */ - findIkConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - const ikConstraints = this.ikConstraints; - for (let i = 0, n = ikConstraints.length; i < n; i++) { - const constraint = ikConstraints[i]; - if (constraint.name == constraintName) return constraint; - } - return null; - } - - /** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of - * this method than to call it multiple times. - * @return May be null. */ - findTransformConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - const transformConstraints = this.transformConstraints; - for (let i = 0, n = transformConstraints.length; i < n; i++) { - const constraint = transformConstraints[i]; - if (constraint.name == constraintName) return constraint; - } - return null; - } - - /** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method - * than to call it multiple times. - * @return May be null. */ - findPathConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - const pathConstraints = this.pathConstraints; - for (let i = 0, n = pathConstraints.length; i < n; i++) { - const constraint = pathConstraints[i]; - if (constraint.name == constraintName) return constraint; - } - return null; - } - - /** Finds a physics constraint by comparing each physics constraint's name. It is more efficient to cache the results of this method - * than to call it multiple times. - * @return May be null. */ - findPhysicsConstraint (constraintName: string) { - if (!constraintName) throw new Error("constraintName cannot be null."); - const physicsConstraints = this.physicsConstraints; - for (let i = 0, n = physicsConstraints.length; i < n; i++) { - const constraint = physicsConstraints[i]; - if (constraint.name == constraintName) return constraint; - } - return null; - } } diff --git a/spine-ts/spine-core/src/SkeletonJson.ts b/spine-ts/spine-core/src/SkeletonJson.ts index 6584bfe0b..d2c59358f 100644 --- a/spine-ts/spine-core/src/SkeletonJson.ts +++ b/spine-ts/spine-core/src/SkeletonJson.ts @@ -27,7 +27,7 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Animation, Timeline, InheritTimeline, AttachmentTimeline, RGBATimeline, RGBTimeline, AlphaTimeline, RGBA2Timeline, RGB2Timeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline2, CurveTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintWindTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintMixTimeline } from "./Animation.js"; +import { Animation, Timeline, InheritTimeline, AttachmentTimeline, RGBATimeline, RGBTimeline, AlphaTimeline, RGBA2Timeline, RGB2Timeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintWindTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintMixTimeline, BoneTimeline2, SliderTimeline, SliderMixTimeline } from "./Animation.js"; import { VertexAttachment, Attachment } from "./attachments/Attachment.js"; import { AttachmentLoader } from "./attachments/AttachmentLoader.js"; import { MeshAttachment } from "./attachments/MeshAttachment.js"; @@ -45,6 +45,7 @@ import { Sequence, SequenceMode } from "./attachments/Sequence.js"; import { SequenceTimeline } from "./Animation.js"; import { HasTextureRegion } from "./attachments/HasTextureRegion.js"; import { PhysicsConstraintData } from "./PhysicsConstraintData.js"; +import { SliderData } from "./SliderData.js"; /** Loads skeleton data in the Spine JSON format. * @@ -59,7 +60,7 @@ export class SkeletonJson { * * See [Scaling](http://esotericsoftware.com/spine-loading-skeleton-data#Scaling) in the Spine Runtimes Guide. */ scale = 1; - private linkedMeshes = new Array(); + private readonly linkedMeshes = new Array(); constructor (attachmentLoader: AttachmentLoader) { this.attachmentLoader = attachmentLoader; @@ -95,14 +96,15 @@ export class SkeletonJson { if (parentName) parent = skeletonData.findBone(parentName); let data = new BoneData(skeletonData.bones.length, boneMap.name, parent); data.length = getValue(boneMap, "length", 0) * scale; - data.x = getValue(boneMap, "x", 0) * scale; - data.y = getValue(boneMap, "y", 0) * scale; - data.rotation = getValue(boneMap, "rotation", 0); - data.scaleX = getValue(boneMap, "scaleX", 1); - data.scaleY = getValue(boneMap, "scaleY", 1); - data.shearX = getValue(boneMap, "shearX", 0); - data.shearY = getValue(boneMap, "shearY", 0); - data.inherit = Utils.enumValue(Inherit, getValue(boneMap, "inherit", "Normal")); + const setup = data.setup; + setup.x = getValue(boneMap, "x", 0) * scale; + setup.y = getValue(boneMap, "y", 0) * scale; + setup.rotation = getValue(boneMap, "rotation", 0); + setup.scaleX = getValue(boneMap, "scaleX", 1); + setup.scaleY = getValue(boneMap, "scaleY", 1); + setup.shearX = getValue(boneMap, "shearX", 0); + setup.shearY = getValue(boneMap, "shearY", 0); + setup.inherit = Utils.enumValue(Inherit, getValue(boneMap, "inherit", "Normal")); data.skinRequired = getValue(boneMap, "skin", false); let color = getValue(boneMap, "color", null); @@ -123,10 +125,10 @@ export class SkeletonJson { let data = new SlotData(skeletonData.slots.length, slotName, boneData); let color: string = getValue(slotMap, "color", null); - if (color) data.color.setFromString(color); + if (color) data.setup.color.setFromString(color); let dark: string = getValue(slotMap, "dark", null); - if (dark) data.darkColor = Color.fromString(dark); + if (dark) data.setup.darkColor = Color.fromString(dark); data.attachmentName = getValue(slotMap, "attachment", null); data.blendMode = Utils.enumValue(BlendMode, getValue(slotMap, "blend", "normal")); @@ -135,207 +137,220 @@ export class SkeletonJson { } } - // IK constraints - if (root.ik) { - for (let i = 0; i < root.ik.length; i++) { - let constraintMap = root.ik[i]; - let data = new IkConstraintData(constraintMap.name); - data.order = getValue(constraintMap, "order", 0); - data.skinRequired = getValue(constraintMap, "skin", false); + // Constraints. + if (root.constraints) { + for (const constraintMap of root.constraints) { + const name = constraintMap.getString("name"); + const skinRequired = getValue(constraintMap, "skin", false); + switch (getValue(constraintMap, "type", false)) { + case "ik": { + const data = new IkConstraintData(name); + data.skinRequired = skinRequired; - for (let ii = 0; ii < constraintMap.bones.length; ii++) { - let bone = skeletonData.findBone(constraintMap.bones[ii]); - if (!bone) throw new Error(`Couldn't find bone ${constraintMap.bones[ii]} for IK constraint ${constraintMap.name}.`); - data.bones.push(bone); - } - - let target = skeletonData.findBone(constraintMap.target);; - if (!target) throw new Error(`Couldn't find target bone ${constraintMap.target} for IK constraint ${constraintMap.name}.`); - data.target = target; - - data.mix = getValue(constraintMap, "mix", 1); - data.softness = getValue(constraintMap, "softness", 0) * scale; - data.bendDirection = getValue(constraintMap, "bendPositive", true) ? 1 : -1; - data.compress = getValue(constraintMap, "compress", false); - data.stretch = getValue(constraintMap, "stretch", false); - data.uniform = getValue(constraintMap, "uniform", false); - - skeletonData.ikConstraints.push(data); - } - } - - // Transform constraints. - if (root.transform) { - for (let i = 0; i < root.transform.length; i++) { - let constraintMap = root.transform[i]; - let data = new TransformConstraintData(constraintMap.name); - data.order = getValue(constraintMap, "order", 0); - data.skinRequired = getValue(constraintMap, "skin", false); - - for (let ii = 0; ii < constraintMap.bones.length; ii++) { - let boneName = constraintMap.bones[ii]; - let bone = skeletonData.findBone(boneName); - if (!bone) throw new Error(`Couldn't find bone ${boneName} for transform constraint ${constraintMap.name}.`); - data.bones.push(bone); - } - - let sourceName: string = constraintMap.source; - let source = skeletonData.findBone(sourceName); - if (!source) throw new Error(`Couldn't find source bone ${sourceName} for transform constraint ${constraintMap.name}.`); - data.source = source; - - data.localSource = getValue(constraintMap, "localSource", false); - data.localTarget = getValue(constraintMap, "localTarget", false); - data.additive = getValue(constraintMap, "additive", false); - data.clamp = getValue(constraintMap, "clamp", false); - - let rotate = false, x = false, y = false, scaleX = false, scaleY = false, shearY = false; - const propertiesEntries = Object.entries(getValue(constraintMap, "properties", {})) as [string, any][]; - for (let ii = 0; ii < propertiesEntries.length; ii++) { - let name = propertiesEntries[ii][0]; - let from: FromProperty; - switch (name) { - case "rotate": from = new FromRotate(); break; - case "x": from = new FromX(); break; - case "y": from = new FromY(); break; - case "scaleX": from = new FromScaleX(); break; - case "scaleY": from = new FromScaleY(); break; - case "shearY": from = new FromShearY(); break; - default: throw new Error("Invalid transform constraint from property: " + name); - } - const fromEntry = propertiesEntries[ii][1]; - from.offset = getValue(fromEntry, "offset", 0) * scale; - const toEntries = Object.entries(getValue(fromEntry, "to", {})) as [string, any][]; - for (let t = 0; t < toEntries.length; t++) { - let name = toEntries[t][0]; - let to: ToProperty; - switch (name) { - case "rotate": { - rotate = true - to = new ToRotate(); - break; - } - case "x": { - x = true - to = new ToX(); - break; - } - case "y": { - y = true - to = new ToY(); - break; - } - case "scaleX": { - scaleX = true - to = new ToScaleX(); - break; - } - case "scaleY": { - scaleY = true - to = new ToScaleY(); - break; - } - case "shearY": { - shearY = true - to = new ToShearY(); - break; - } - default: throw new Error("Invalid transform constraint to property: " + name); + for (let ii = 0; ii < constraintMap.bones.length; ii++) { + let bone = skeletonData.findBone(constraintMap.bones[ii]); + if (!bone) throw new Error(`Couldn't find bone ${constraintMap.bones[ii]} for IK constraint ${name}.`); + data.bones.push(bone); } - let toEntry = toEntries[t][1]; - to.offset = getValue(toEntry, "offset", 0) * scale; - to.max = getValue(toEntry, "max", 1) * scale; - to.scale = getValue(toEntry, "scale", 1); - from.to.push(to); + + const targetName = constraintMap.target; + const target = skeletonData.findBone(targetName); + if (!target) throw new Error(`Couldn't find target bone ${targetName} for IK constraint ${name}.`); + data.target = target; + + data.uniform = getValue(constraintMap, "uniform", false); + const setup = data.setup; + setup.mix = getValue(constraintMap, "mix", 1); + setup.softness = getValue(constraintMap, "softness", 0) * scale; + setup.bendDirection = getValue(constraintMap, "bendPositive", true) ? 1 : -1; + setup.compress = getValue(constraintMap, "compress", false); + setup.stretch = getValue(constraintMap, "stretch", false); + + skeletonData.constraints.push(data); + break; + } + case "transform": { + const data = new TransformConstraintData(name); + data.skinRequired = skinRequired; + + for (let ii = 0; ii < constraintMap.bones.length; ii++) { + let boneName = constraintMap.bones[ii]; + let bone = skeletonData.findBone(boneName); + if (!bone) throw new Error(`Couldn't find bone ${boneName} for transform constraint ${constraintMap.name}.`); + data.bones.push(bone); + } + + const sourceName: string = constraintMap.source; + const source = skeletonData.findBone(sourceName); + if (!source) throw new Error(`Couldn't find source bone ${sourceName} for transform constraint ${constraintMap.name}.`); + data.source = source; + + data.localSource = getValue(constraintMap, "localSource", false); + data.localTarget = getValue(constraintMap, "localTarget", false); + data.additive = getValue(constraintMap, "additive", false); + data.clamp = getValue(constraintMap, "clamp", false); + + let rotate = false, x = false, y = false, scaleX = false, scaleY = false, shearY = false; + const fromEntries = Object.entries(getValue(constraintMap, "properties", {})) as [string, any][]; + for (const [name, fromEntry] of fromEntries) { + const from = this.fromProperty(name); + const fromScale = this.propertyScale(name, scale); + from.offset = getValue(fromEntry, "offset", 0) * fromScale; + const toEntries = Object.entries(getValue(fromEntry, "to", {})) as [string, any][]; + for (const [name, toEntry] of toEntries) { + let toScale = 1; + let to: ToProperty; + switch (name) { + case "rotate": { + rotate = true + to = new ToRotate(); + break; + } + case "x": { + x = true + to = new ToX(); + toScale = scale; + break; + } + case "y": { + y = true + to = new ToY(); + toScale = scale; + break; + } + case "scaleX": { + scaleX = true + to = new ToScaleX(); + break; + } + case "scaleY": { + scaleY = true + to = new ToScaleY(); + break; + } + case "shearY": { + shearY = true + to = new ToShearY(); + break; + } + default: throw new Error("Invalid transform constraint to property: " + name); + } + to.offset = getValue(toEntry, "offset", 0) * toScale; + to.max = getValue(toEntry, "max", 1) * toScale; + to.scale = getValue(toEntry, "scale", 1) * toScale / fromScale; + from.to.push(to); + } + if (from.to.length > 0) data.properties.push(from); + } + + data.offsets[0] = getValue(constraintMap, "rotation", 0); + data.offsets[1] = getValue(constraintMap, "x", 0) * scale; + data.offsets[2] = getValue(constraintMap, "y", 0) * scale; + data.offsets[3] = getValue(constraintMap, "scaleX", 0); + data.offsets[4] = getValue(constraintMap, "scaleY", 0); + data.offsets[5] = getValue(constraintMap, "shearY", 0); + + const setup = data.setup; + 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); + if (scaleX) setup.mixScaleX = getValue(constraintMap, "mixScaleX", 1); + if (scaleY) setup.mixScaleY = getValue(constraintMap, "mixScaleY", setup.mixScaleX); + if (shearY) setup.mixShearY = getValue(constraintMap, "mixShearY", 1); + + skeletonData.constraints.push(data); + } + case "path": { + const data = new PathConstraintData(name); + data.skinRequired = skinRequired; + + for (let ii = 0; ii < constraintMap.bones.length; ii++) { + let boneName = constraintMap.bones[ii]; + let bone = skeletonData.findBone(boneName); + if (!bone) throw new Error(`Couldn't find bone ${boneName} for path constraint ${constraintMap.name}.`); + data.bones.push(bone); + } + + const slotName: string = constraintMap.slot; + const slot = skeletonData.findSlot(slotName); + if (!slot) throw new Error(`Couldn't find slot ${slotName} for path constraint ${constraintMap.name}.`); + data.slot = slot; + + data.positionMode = Utils.enumValue(PositionMode, getValue(constraintMap, "positionMode", "Percent")); + 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; + setup.position = getValue(constraintMap, "position", 0); + if (data.positionMode == PositionMode.Fixed) setup.position *= scale; + setup.spacing = getValue(constraintMap, "spacing", 0); + if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) setup.spacing *= scale; + setup.mixRotate = getValue(constraintMap, "mixRotate", 1); + setup.mixX = getValue(constraintMap, "mixX", 1); + setup.mixY = getValue(constraintMap, "mixY", setup.mixX); + + skeletonData.constraints.push(data); + break; + } + case "physics": { + const data = new PhysicsConstraintData(name); + data.skinRequired = skinRequired; + + const boneName: string = constraintMap.bone; + const bone = skeletonData.findBone(boneName); + if (bone == null) throw new Error("Physics bone not found: " + boneName); + data.bone = bone; + + data.x = getValue(constraintMap, "x", 0); + data.y = getValue(constraintMap, "y", 0); + data.rotate = getValue(constraintMap, "rotate", 0); + data.scaleX = getValue(constraintMap, "scaleX", 0); + data.shearX = getValue(constraintMap, "shearX", 0); + data.limit = getValue(constraintMap, "limit", 5000) * scale; + data.step = 1 / getValue(constraintMap, "fps", 60); + const setup = data.setup; + setup.inertia = getValue(constraintMap, "inertia", 0.5); + setup.strength = getValue(constraintMap, "strength", 100); + setup.damping = getValue(constraintMap, "damping", 0.85); + setup.massInverse = 1 / getValue(constraintMap, "mass", 1); + setup.wind = getValue(constraintMap, "wind", 0); + setup.gravity = getValue(constraintMap, "gravity", 0); + setup.mix = getValue(constraintMap, "mix", 1); + data.inertiaGlobal = getValue(constraintMap, "inertiaGlobal", false); + data.strengthGlobal = getValue(constraintMap, "strengthGlobal", false); + data.dampingGlobal = getValue(constraintMap, "dampingGlobal", false); + data.massGlobal = getValue(constraintMap, "massGlobal", false); + data.windGlobal = getValue(constraintMap, "windGlobal", false); + data.gravityGlobal = getValue(constraintMap, "gravityGlobal", false); + data.mixGlobal = getValue(constraintMap, "mixGlobal", false); + + skeletonData.constraints.push(data); + break; + } + case "slider": { + const data = new SliderData(name); + data.skinRequired = skinRequired; + + 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); + + const boneName: string = constraintMap.bone; + if (boneName) { + data.bone = skeletonData.findBone(boneName); + if (!data.bone) throw new Error("Slider bone not found: " + boneName); + const property = constraintMap.property; + data.property = this.fromProperty(property); + data.property.offset = constraintMap.getFloat("offset", 0) * this.propertyScale(property, scale); + data.scale = getValue(constraintMap, "scale", 1); + data.local = getValue(constraintMap, "local", false); + } + + skeletonData.constraints.push(data); + break; } - if (from.to.length > 0) data.properties.push(from); } - - data.offsetX = getValue(constraintMap, "x", 0); - data.offsetY = getValue(constraintMap, "y", 0); - if (rotate) data.mixRotate = getValue(constraintMap, "mixRotate", 1); - if (x) data.mixX = getValue(constraintMap, "mixX", 1); - if (y) data.mixY = getValue(constraintMap, "mixY", data.mixX); - if (scaleX) data.mixScaleX = getValue(constraintMap, "mixScaleX", 1); - if (scaleY) data.mixScaleY = getValue(constraintMap, "mixScaleY", data.mixScaleX); - if (shearY) data.mixShearY = getValue(constraintMap, "mixShearY", 1); - - skeletonData.transformConstraints.push(data); - } - } - - // Path constraints. - if (root.path) { - for (let i = 0; i < root.path.length; i++) { - let constraintMap = root.path[i]; - let data = new PathConstraintData(constraintMap.name); - data.order = getValue(constraintMap, "order", 0); - data.skinRequired = getValue(constraintMap, "skin", false); - - for (let ii = 0; ii < constraintMap.bones.length; ii++) { - let boneName = constraintMap.bones[ii]; - let bone = skeletonData.findBone(boneName); - if (!bone) throw new Error(`Couldn't find bone ${boneName} for path constraint ${constraintMap.name}.`); - data.bones.push(bone); - } - - let slotName: string = constraintMap.slot; - let slot = skeletonData.findSlot(slotName); - if (!slot) throw new Error(`Couldn't find slot ${slotName} for path constraint ${constraintMap.name}.`); - data.slot = slot; - - data.positionMode = Utils.enumValue(PositionMode, getValue(constraintMap, "positionMode", "Percent")); - data.spacingMode = Utils.enumValue(SpacingMode, getValue(constraintMap, "spacingMode", "Length")); - data.rotateMode = Utils.enumValue(RotateMode, getValue(constraintMap, "rotateMode", "Tangent")); - data.offsetRotation = getValue(constraintMap, "rotation", 0); - data.position = getValue(constraintMap, "position", 0); - if (data.positionMode == PositionMode.Fixed) data.position *= scale; - data.spacing = getValue(constraintMap, "spacing", 0); - if (data.spacingMode == SpacingMode.Length || data.spacingMode == SpacingMode.Fixed) data.spacing *= scale; - data.mixRotate = getValue(constraintMap, "mixRotate", 1); - data.mixX = getValue(constraintMap, "mixX", 1); - data.mixY = getValue(constraintMap, "mixY", data.mixX); - - skeletonData.pathConstraints.push(data); - } - } - - // Physics constraints. - if (root.physics) { - for (let i = 0; i < root.physics.length; i++) { - const constraintMap = root.physics[i]; - const data = new PhysicsConstraintData(constraintMap.name); - data.order = getValue(constraintMap, "order", 0); - data.skinRequired = getValue(constraintMap, "skin", false); - - const boneName = constraintMap.bone; - const bone = skeletonData.findBone(boneName); - if (bone == null) throw new Error("Physics bone not found: " + boneName); - data.bone = bone; - - data.x = getValue(constraintMap, "x", 0); - data.y = getValue(constraintMap, "y", 0); - data.rotate = getValue(constraintMap, "rotate", 0); - data.scaleX = getValue(constraintMap, "scaleX", 0); - data.shearX = getValue(constraintMap, "shearX", 0); - data.limit = getValue(constraintMap, "limit", 5000) * scale; - data.step = 1 / getValue(constraintMap, "fps", 60); - data.inertia = getValue(constraintMap, "inertia", 1); - data.strength = getValue(constraintMap, "strength", 100); - data.damping = getValue(constraintMap, "damping", 1); - data.massInverse = 1 / getValue(constraintMap, "mass", 1); - data.wind = getValue(constraintMap, "wind", 0); - data.gravity = getValue(constraintMap, "gravity", 0); - data.mix = getValue(constraintMap, "mix", 1); - data.inertiaGlobal = getValue(constraintMap, "inertiaGlobal", false); - data.strengthGlobal = getValue(constraintMap, "strengthGlobal", false); - data.dampingGlobal = getValue(constraintMap, "dampingGlobal", false); - data.massGlobal = getValue(constraintMap, "massGlobal", false); - data.windGlobal = getValue(constraintMap, "windGlobal", false); - data.gravityGlobal = getValue(constraintMap, "gravityGlobal", false); - data.mixGlobal = getValue(constraintMap, "mixGlobal", false); - - skeletonData.physicsConstraints.push(data); } } @@ -357,7 +372,7 @@ export class SkeletonJson { if (skinMap.ik) { for (let ii = 0; ii < skinMap.ik.length; ii++) { let constraintName = skinMap.ik[ii]; - let constraint = skeletonData.findIkConstraint(constraintName); + let constraint = skeletonData.findConstraint(constraintName, IkConstraintData); if (!constraint) throw new Error(`Couldn't find IK constraint ${constraintName} for skin ${skinMap.name}.`); skin.constraints.push(constraint); } @@ -366,7 +381,7 @@ export class SkeletonJson { if (skinMap.transform) { for (let ii = 0; ii < skinMap.transform.length; ii++) { let constraintName = skinMap.transform[ii]; - let constraint = skeletonData.findTransformConstraint(constraintName); + let constraint = skeletonData.findConstraint(constraintName, TransformConstraintData); if (!constraint) throw new Error(`Couldn't find transform constraint ${constraintName} for skin ${skinMap.name}.`); skin.constraints.push(constraint); } @@ -375,7 +390,7 @@ export class SkeletonJson { if (skinMap.path) { for (let ii = 0; ii < skinMap.path.length; ii++) { let constraintName = skinMap.path[ii]; - let constraint = skeletonData.findPathConstraint(constraintName); + let constraint = skeletonData.findConstraint(constraintName, PathConstraintData); if (!constraint) throw new Error(`Couldn't find path constraint ${constraintName} for skin ${skinMap.name}.`); skin.constraints.push(constraint); } @@ -384,12 +399,21 @@ export class SkeletonJson { if (skinMap.physics) { for (let ii = 0; ii < skinMap.physics.length; ii++) { let constraintName = skinMap.physics[ii]; - let constraint = skeletonData.findPhysicsConstraint(constraintName); + let constraint = skeletonData.findConstraint(constraintName, PhysicsConstraintData); if (!constraint) throw new Error(`Couldn't find physics constraint ${constraintName} for skin ${skinMap.name}.`); skin.constraints.push(constraint); } } + if (skinMap.slider) { + for (let ii = 0; ii < skinMap.slider.length; ii++) { + let constraintName = skinMap.slider[ii]; + let constraint = skeletonData.findConstraint(constraintName, SliderData); + if (!constraint) throw new Error(`Couldn't find slider constraint ${constraintName} for skin ${skinMap.name}.`); + skin.constraints.push(constraint); + } + } + for (let slotName in skinMap.attachments) { let slot = skeletonData.findSlot(slotName); if (!slot) throw new Error(`Couldn't find slot ${slotName} for skin ${skinMap.name}.`); @@ -442,9 +466,45 @@ export class SkeletonJson { } } + // Slider animations. + if (root.constraints) { + for (let animationName in root.constraints) { + let animationMap = root.constraints[animationName]; + if (animationMap.type === "slider") { + const data = skeletonData.findConstraint(animationMap.name, SliderData)!; + const animationName = animationMap.animation; + const animation = skeletonData.findAnimation(animationName); + if (!animation) throw new Error("Slider animation not found: " + animationName); + data.animation = animation; + } + } + } + return skeletonData; } + private fromProperty (type: string): FromProperty { + let from: FromProperty; + switch (type) { + case "rotate": from = new FromRotate(); break; + case "x": from = new FromX(); break; + case "y": from = new FromY(); break; + case "scaleX": from = new FromScaleX(); break; + case "scaleY": from = new FromScaleY(); break; + case "shearY": from = new FromShearY(); break; + default: throw new Error("Invalid transform constraint from property: " + type); + } + return from; + } + + private propertyScale (type: string, scale: number) { + switch (type) { + case "x": + case "y": return scale; + default: return 1; + }; + } + readAttachment (map: any, skin: Skin, slotIndex: number, name: string, skeletonData: SkeletonData): Attachment | null { let scale = this.scale; name = getValue(map, "name", name); @@ -610,147 +670,156 @@ export class SkeletonJson { let timelineMap = slotMap[timelineName]; if (!timelineMap) continue; let frames = timelineMap.length; - if (timelineName == "attachment") { - let timeline = new AttachmentTimeline(frames, slotIndex); - for (let frame = 0; frame < frames; frame++) { - let keyMap = timelineMap[frame]; - timeline.setFrame(frame, getValue(keyMap, "time", 0), getValue(keyMap, "name", null)); + + switch (timelineName) { + case "attachment": { + let timeline = new AttachmentTimeline(frames, slotIndex); + for (let frame = 0; frame < frames; frame++) { + let keyMap = timelineMap[frame]; + timeline.setFrame(frame, getValue(keyMap, "time", 0), getValue(keyMap, "name", null)); + } + timelines.push(timeline); + break; } - timelines.push(timeline); + case "rgba": { + let timeline = new RGBATimeline(frames, frames << 2, slotIndex); + let keyMap = timelineMap[0]; + let time = getValue(keyMap, "time", 0); + let color = Color.fromString(keyMap.color); - } else if (timelineName == "rgba") { - let timeline = new RGBATimeline(frames, frames << 2, slotIndex); - let keyMap = timelineMap[0]; - let time = getValue(keyMap, "time", 0); - let color = Color.fromString(keyMap.color); + for (let frame = 0, bezier = 0; ; frame++) { + timeline.setFrame(frame, time, color.r, color.g, color.b, color.a); + let nextMap = timelineMap[frame + 1]; + if (!nextMap) { + timeline.shrink(bezier); + break; + } + let time2 = getValue(nextMap, "time", 0); + let newColor = Color.fromString(nextMap.color); + let curve = keyMap.curve; + if (curve) { + bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); + bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); + bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); + bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color.a, newColor.a, 1); + } + time = time2; + color = newColor; + keyMap = nextMap; + } - for (let frame = 0, bezier = 0; ; frame++) { - timeline.setFrame(frame, time, color.r, color.g, color.b, color.a); - let nextMap = timelineMap[frame + 1]; - if (!nextMap) { - timeline.shrink(bezier); - break; - } - let time2 = getValue(nextMap, "time", 0); - let newColor = Color.fromString(nextMap.color); - let curve = keyMap.curve; - if (curve) { - bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); - bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); - bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); - bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color.a, newColor.a, 1); - } - time = time2; - color = newColor; - keyMap = nextMap; + timelines.push(timeline); + break; } + case "rgb": { + let timeline = new RGBTimeline(frames, frames * 3, slotIndex); + let keyMap = timelineMap[0]; + let time = getValue(keyMap, "time", 0); + let color = Color.fromString(keyMap.color); - timelines.push(timeline); - - } else if (timelineName == "rgb") { - let timeline = new RGBTimeline(frames, frames * 3, slotIndex); - let keyMap = timelineMap[0]; - let time = getValue(keyMap, "time", 0); - let color = Color.fromString(keyMap.color); - - for (let frame = 0, bezier = 0; ; frame++) { - timeline.setFrame(frame, time, color.r, color.g, color.b); - let nextMap = timelineMap[frame + 1]; - if (!nextMap) { - timeline.shrink(bezier); - break; + for (let frame = 0, bezier = 0; ; frame++) { + timeline.setFrame(frame, time, color.r, color.g, color.b); + let nextMap = timelineMap[frame + 1]; + if (!nextMap) { + timeline.shrink(bezier); + break; + } + let time2 = getValue(nextMap, "time", 0); + let newColor = Color.fromString(nextMap.color); + let curve = keyMap.curve; + if (curve) { + bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); + bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); + bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); + } + time = time2; + color = newColor; + keyMap = nextMap; } - let time2 = getValue(nextMap, "time", 0); - let newColor = Color.fromString(nextMap.color); - let curve = keyMap.curve; - if (curve) { - bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); - bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); - bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); - } - time = time2; - color = newColor; - keyMap = nextMap; + + timelines.push(timeline); + break; } - - timelines.push(timeline); - - } else if (timelineName == "alpha") { - timelines.push(readTimeline1(timelineMap, new AlphaTimeline(frames, frames, slotIndex), 0, 1)); - } else if (timelineName == "rgba2") { - let timeline = new RGBA2Timeline(frames, frames * 7, slotIndex); - - let keyMap = timelineMap[0]; - let time = getValue(keyMap, "time", 0); - let color = Color.fromString(keyMap.light); - let color2 = Color.fromString(keyMap.dark); - - for (let frame = 0, bezier = 0; ; frame++) { - timeline.setFrame(frame, time, color.r, color.g, color.b, color.a, color2.r, color2.g, color2.b); - let nextMap = timelineMap[frame + 1]; - if (!nextMap) { - timeline.shrink(bezier); - break; - } - let time2 = getValue(nextMap, "time", 0); - let newColor = Color.fromString(nextMap.light); - let newColor2 = Color.fromString(nextMap.dark); - let curve = keyMap.curve; - if (curve) { - bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); - bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); - bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); - bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color.a, newColor.a, 1); - bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, color2.r, newColor2.r, 1); - bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, color2.g, newColor2.g, 1); - bezier = readCurve(curve, timeline, bezier, frame, 6, time, time2, color2.b, newColor2.b, 1); - } - time = time2; - color = newColor; - color2 = newColor2; - keyMap = nextMap; + case "alpha": { + readTimeline1(timelines, timelineMap, new AlphaTimeline(frames, frames, slotIndex), 0, 1); + break; } + case "rgba2": { + let timeline = new RGBA2Timeline(frames, frames * 7, slotIndex); - timelines.push(timeline); + let keyMap = timelineMap[0]; + let time = getValue(keyMap, "time", 0); + let color = Color.fromString(keyMap.light); + let color2 = Color.fromString(keyMap.dark); - } else if (timelineName == "rgb2") { - let timeline = new RGB2Timeline(frames, frames * 6, slotIndex); - - let keyMap = timelineMap[0]; - let time = getValue(keyMap, "time", 0); - let color = Color.fromString(keyMap.light); - let color2 = Color.fromString(keyMap.dark); - - for (let frame = 0, bezier = 0; ; frame++) { - timeline.setFrame(frame, time, color.r, color.g, color.b, color2.r, color2.g, color2.b); - let nextMap = timelineMap[frame + 1]; - if (!nextMap) { - timeline.shrink(bezier); - break; + for (let frame = 0, bezier = 0; ; frame++) { + timeline.setFrame(frame, time, color.r, color.g, color.b, color.a, color2.r, color2.g, color2.b); + let nextMap = timelineMap[frame + 1]; + if (!nextMap) { + timeline.shrink(bezier); + break; + } + let time2 = getValue(nextMap, "time", 0); + let newColor = Color.fromString(nextMap.light); + let newColor2 = Color.fromString(nextMap.dark); + let curve = keyMap.curve; + if (curve) { + bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); + bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); + bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); + bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color.a, newColor.a, 1); + bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, color2.r, newColor2.r, 1); + bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, color2.g, newColor2.g, 1); + bezier = readCurve(curve, timeline, bezier, frame, 6, time, time2, color2.b, newColor2.b, 1); + } + time = time2; + color = newColor; + color2 = newColor2; + keyMap = nextMap; } - let time2 = getValue(nextMap, "time", 0); - let newColor = Color.fromString(nextMap.light); - let newColor2 = Color.fromString(nextMap.dark); - let curve = keyMap.curve; - if (curve) { - bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); - bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); - bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); - bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color2.r, newColor2.r, 1); - bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, color2.g, newColor2.g, 1); - bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, color2.b, newColor2.b, 1); - } - time = time2; - color = newColor; - color2 = newColor2; - keyMap = nextMap; + + timelines.push(timeline); + break; } + case "rgb2": { + let timeline = new RGB2Timeline(frames, frames * 6, slotIndex); - timelines.push(timeline); - } else { - throw new Error("Invalid timeline type for a slot: " + timelineMap.name + " (" + slotMap.name + ")"); + let keyMap = timelineMap[0]; + let time = getValue(keyMap, "time", 0); + let color = Color.fromString(keyMap.light); + let color2 = Color.fromString(keyMap.dark); + + for (let frame = 0, bezier = 0; ; frame++) { + timeline.setFrame(frame, time, color.r, color.g, color.b, color2.r, color2.g, color2.b); + let nextMap = timelineMap[frame + 1]; + if (!nextMap) { + timeline.shrink(bezier); + break; + } + let time2 = getValue(nextMap, "time", 0); + let newColor = Color.fromString(nextMap.light); + let newColor2 = Color.fromString(nextMap.dark); + let curve = keyMap.curve; + if (curve) { + bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1); + bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1); + bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1); + bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color2.r, newColor2.r, 1); + bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, color2.g, newColor2.g, 1); + bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, color2.b, newColor2.b, 1); + } + time = time2; + color = newColor; + color2 = newColor2; + keyMap = nextMap; + } + + timelines.push(timeline); + break; + } + default: + throw new Error("Invalid timeline type for a slot: " + timelineMap.name + " (" + slotMap.name + ")"); } - } } } @@ -767,45 +836,29 @@ export class SkeletonJson { let frames = timelineMap.length; if (frames == 0) continue; - if (timelineName === "rotate") { - timelines.push(readTimeline1(timelineMap, new RotateTimeline(frames, frames, boneIndex), 0, 1)); - } else if (timelineName === "translate") { - let timeline = new TranslateTimeline(frames, frames << 1, boneIndex); - timelines.push(readTimeline2(timelineMap, timeline, "x", "y", 0, scale)); - } else if (timelineName === "translatex") { - let timeline = new TranslateXTimeline(frames, frames, boneIndex); - timelines.push(readTimeline1(timelineMap, timeline, 0, scale)); - } else if (timelineName === "translatey") { - let timeline = new TranslateYTimeline(frames, frames, boneIndex); - timelines.push(readTimeline1(timelineMap, timeline, 0, scale)); - } else if (timelineName === "scale") { - let timeline = new ScaleTimeline(frames, frames << 1, boneIndex); - timelines.push(readTimeline2(timelineMap, timeline, "x", "y", 1, 1)); - } else if (timelineName === "scalex") { - let timeline = new ScaleXTimeline(frames, frames, boneIndex); - timelines.push(readTimeline1(timelineMap, timeline, 1, 1)); - } else if (timelineName === "scaley") { - let timeline = new ScaleYTimeline(frames, frames, boneIndex); - timelines.push(readTimeline1(timelineMap, timeline, 1, 1)); - } else if (timelineName === "shear") { - let timeline = new ShearTimeline(frames, frames << 1, boneIndex); - timelines.push(readTimeline2(timelineMap, timeline, "x", "y", 0, 1)); - } else if (timelineName === "shearx") { - let timeline = new ShearXTimeline(frames, frames, boneIndex); - timelines.push(readTimeline1(timelineMap, timeline, 0, 1)); - } else if (timelineName === "sheary") { - let timeline = new ShearYTimeline(frames, frames, boneIndex); - timelines.push(readTimeline1(timelineMap, timeline, 0, 1)); - } else if (timelineName === "inherit") { - let timeline = new InheritTimeline(frames, bone.index); - for (let frame = 0; frame < timelineMap.length; frame++) { - let aFrame = timelineMap[frame]; - timeline.setFrame(frame, getValue(aFrame, "time", 0), Utils.enumValue(Inherit, getValue(aFrame, "inherit", "Normal"))); - } - timelines.push(timeline); - } else { - throw new Error("Invalid timeline type for a bone: " + timelineMap.name + " (" + boneMap.name + ")"); + switch (timelineName) { + case "rotate": readTimeline1(timelines, timelineMap, new RotateTimeline(frames, frames, boneIndex), 0, 1); break; + case "translate": readTimeline2(timelines, timelineMap, new TranslateTimeline(frames, frames << 1, boneIndex), "x", "y", 0, scale); break; + case "translatex": readTimeline1(timelines, timelineMap, new TranslateXTimeline(frames, frames, boneIndex), 0, scale); break; + case "translatey": readTimeline1(timelines, timelineMap, new TranslateYTimeline(frames, frames, boneIndex), 0, scale); break; + case "scale": readTimeline2(timelines, timelineMap, new ScaleTimeline(frames, frames << 1, boneIndex), "x", "y", 1, 1); break; + case "scalex": readTimeline1(timelines, timelineMap, new ScaleXTimeline(frames, frames, boneIndex), 1, 1); break; + case "scaley": readTimeline1(timelines, timelineMap, new ScaleYTimeline(frames, frames, boneIndex), 1, 1); break; + case "shear": readTimeline2(timelines, timelineMap, new ShearTimeline(frames, frames << 1, boneIndex), "x", "y", 0, 1); break; + case "shearx": readTimeline1(timelines, timelineMap, new ShearXTimeline(frames, frames, boneIndex), 0, 1); break; + case "sheary": readTimeline1(timelines, timelineMap, new ShearYTimeline(frames, frames, boneIndex), 0, 1); break; + case "inherit": + const timeline = new InheritTimeline(frames, bone.index); + for (let frame = 0; frame < timelineMap.length; frame++) { + let aFrame = timelineMap[frame]; + timeline.setFrame(frame, getValue(aFrame, "time", 0), Utils.enumValue(Inherit, getValue(aFrame, "inherit", "Normal"))); + } + timelines.push(timeline); + break; + default: + throw new Error("Invalid timeline type for a bone: " + timelineMap.name + " (" + boneMap.name + ")"); } + } } } @@ -817,10 +870,10 @@ export class SkeletonJson { let keyMap = constraintMap[0]; if (!keyMap) continue; - let constraint = skeletonData.findIkConstraint(constraintName); + let constraint = skeletonData.findConstraint(constraintName, IkConstraintData); if (!constraint) throw new Error("IK Constraint not found: " + constraintName); - let constraintIndex = skeletonData.ikConstraints.indexOf(constraint); - let timeline = new IkConstraintTimeline(constraintMap.length, constraintMap.length << 1, constraintIndex); + const timeline = new IkConstraintTimeline(constraintMap.length, constraintMap.length << 1, + skeletonData.constraints.indexOf(constraint)); let time = getValue(keyMap, "time", 0); let mix = getValue(keyMap, "mix", 1); @@ -859,18 +912,18 @@ export class SkeletonJson { let keyMap = timelineMap[0]; if (!keyMap) continue; - let constraint = skeletonData.findTransformConstraint(constraintName); + let constraint = skeletonData.findConstraint(constraintName, TransformConstraintData); if (!constraint) throw new Error("Transform constraint not found: " + constraintName); - let constraintIndex = skeletonData.transformConstraints.indexOf(constraint); - let timeline = new TransformConstraintTimeline(timelineMap.length, timelineMap.length * 6, constraintIndex); + let timeline = new TransformConstraintTimeline(timelineMap.length, timelineMap.length * 6, + skeletonData.constraints.indexOf(constraint)); let time = getValue(keyMap, "time", 0); - let mixRotate = getValue(keyMap, "mixRotate", 1); - let mixX = getValue(keyMap, "mixX", 1); + let mixRotate = getValue(keyMap, "mixRotate", 0); + let mixX = getValue(keyMap, "mixX", 0); let mixY = getValue(keyMap, "mixY", mixX); - let mixScaleX = getValue(keyMap, "mixScaleX", 1); + let mixScaleX = getValue(keyMap, "mixScaleX", 0); let mixScaleY = getValue(keyMap, "mixScaleY", mixScaleX); - let mixShearY = getValue(keyMap, "mixShearY", 1); + let mixShearY = getValue(keyMap, "mixShearY", 0); for (let frame = 0, bezier = 0; ; frame++) { timeline.setFrame(frame, time, mixRotate, mixX, mixY, mixScaleX, mixScaleY, mixShearY); @@ -913,52 +966,59 @@ export class SkeletonJson { // Path constraint timelines. if (map.path) { for (let constraintName in map.path) { - let constraintMap = map.path[constraintName]; - let constraint = skeletonData.findPathConstraint(constraintName); + const constraintMap = map.path[constraintName]; + const constraint = skeletonData.findConstraint(constraintName, PathConstraintData); if (!constraint) throw new Error("Path constraint not found: " + constraintName); - let constraintIndex = skeletonData.pathConstraints.indexOf(constraint); - for (let timelineName in constraintMap) { - let timelineMap = constraintMap[timelineName]; + const index = skeletonData.constraints.indexOf(constraint); + for (const timelineName in constraintMap) { + const timelineMap = constraintMap[timelineName]; let keyMap = timelineMap[0]; if (!keyMap) continue; - let frames = timelineMap.length; - if (timelineName === "position") { - let timeline = new PathConstraintPositionTimeline(frames, frames, constraintIndex); - timelines.push(readTimeline1(timelineMap, timeline, 0, constraint.positionMode == PositionMode.Fixed ? scale : 1)); - } else if (timelineName === "spacing") { - let timeline = new PathConstraintSpacingTimeline(frames, frames, constraintIndex); - timelines.push(readTimeline1(timelineMap, timeline, 0, constraint.spacingMode == SpacingMode.Length || constraint.spacingMode == SpacingMode.Fixed ? scale : 1)); - } else if (timelineName === "mix") { - let timeline = new PathConstraintMixTimeline(frames, frames * 3, constraintIndex); - let time = getValue(keyMap, "time", 0); - let mixRotate = getValue(keyMap, "mixRotate", 1); - let mixX = getValue(keyMap, "mixX", 1); - let mixY = getValue(keyMap, "mixY", mixX); - for (let frame = 0, bezier = 0; ; frame++) { - timeline.setFrame(frame, time, mixRotate, mixX, mixY); - let nextMap = timelineMap[frame + 1]; - if (!nextMap) { - timeline.shrink(bezier); - break; - } - let time2 = getValue(nextMap, "time", 0); - let mixRotate2 = getValue(nextMap, "mixRotate", 1); - let mixX2 = getValue(nextMap, "mixX", 1); - let mixY2 = getValue(nextMap, "mixY", mixX2); - let curve = keyMap.curve; - if (curve) { - bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1); - bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1); - bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1); - } - time = time2; - mixRotate = mixRotate2; - mixX = mixX2; - mixY = mixY2; - keyMap = nextMap; + const frames = timelineMap.length; + switch (timelineName) { + case "position": { + const timeline = new PathConstraintPositionTimeline(frames, frames, index); + readTimeline1(timelines, timelineMap, timeline, 0, constraint.positionMode == PositionMode.Fixed ? scale : 1); + break; + } + case "spacing": { + const timeline = new PathConstraintSpacingTimeline(frames, frames, index); + readTimeline1(timelines, timelineMap, timeline, 0, constraint.spacingMode == SpacingMode.Length || constraint.spacingMode == SpacingMode.Fixed ? scale : 1); + break; + } + case "mix": { + const timeline = new PathConstraintMixTimeline(frames, frames * 3, index); + let time = getValue(keyMap, "time", 0); + let mixRotate = getValue(keyMap, "mixRotate", 1); + let mixX = getValue(keyMap, "mixX", 1); + let mixY = getValue(keyMap, "mixY", mixX); + for (let frame = 0, bezier = 0; ; frame++) { + timeline.setFrame(frame, time, mixRotate, mixX, mixY); + let nextMap = timelineMap[frame + 1]; + if (!nextMap) { + timeline.shrink(bezier); + break; + } + let time2 = getValue(nextMap, "time", 0); + let mixRotate2 = getValue(nextMap, "mixRotate", 1); + let mixX2 = getValue(nextMap, "mixX", 1); + let mixY2 = getValue(nextMap, "mixY", mixX2); + let curve = keyMap.curve; + if (curve) { + bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, mixRotate, mixRotate2, 1); + bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, mixX, mixX2, 1); + bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, mixY, mixY2, 1); + } + time = time2; + mixRotate = mixRotate2; + mixX = mixX2; + mixY = mixY2; + keyMap = nextMap; + } + timelines.push(timeline); + break; } - timelines.push(timeline); } } } @@ -966,46 +1026,62 @@ export class SkeletonJson { // Physics constraint timelines. if (map.physics) { - for (let constraintName in map.physics) { - let constraintMap = map.physics[constraintName]; - let constraintIndex = -1; + for (const constraintName in map.physics) { + const constraintMap = map.physics[constraintName]; + let index = -1; if (constraintName.length > 0) { - let constraint = skeletonData.findPhysicsConstraint(constraintName); + const constraint = skeletonData.findConstraint(constraintName, PhysicsConstraintData); if (!constraint) throw new Error("Physics constraint not found: " + constraintName); - constraintIndex = skeletonData.physicsConstraints.indexOf(constraint); + index = skeletonData.constraints.indexOf(constraint); } - for (let timelineName in constraintMap) { - let timelineMap = constraintMap[timelineName]; + for (const timelineName in constraintMap) { + const timelineMap = constraintMap[timelineName]; let keyMap = timelineMap[0]; if (!keyMap) continue; - let frames = timelineMap.length; - if (timelineName == "reset") { - const timeline = new PhysicsConstraintResetTimeline(frames, constraintIndex); + const frames = timelineMap.length; + let timeline; + let defaultValue = 0; + if (timelineName === "reset") { + const resetTimeline = new PhysicsConstraintResetTimeline(frames, index); for (let frame = 0; keyMap != null; keyMap = timelineMap[frame + 1], frame++) - timeline.setFrame(frame, getValue(keyMap, "time", 0)); - timelines.push(timeline); + resetTimeline.setFrame(frame, getValue(keyMap, "time", 0)); + timelines.push(resetTimeline); continue; } + switch (timelineName) { + case "inertia": timeline = new PhysicsConstraintInertiaTimeline(frames, frames, index); break; + case "strength": timeline = new PhysicsConstraintStrengthTimeline(frames, frames, index); break; + case "damping": timeline = new PhysicsConstraintDampingTimeline(frames, frames, index); break; + case "mass": timeline = new PhysicsConstraintMassTimeline(frames, frames, index); break; + case "wind": timeline = new PhysicsConstraintWindTimeline(frames, frames, index); break; + case "gravity": timeline = new PhysicsConstraintGravityTimeline(frames, frames, index); break; + case "mix": timeline = new PhysicsConstraintMixTimeline(frames, frames, index); break; + default: continue; + } + readTimeline1(timelines, timelineMap, timeline, 0, 1); + } + } + } - let timeline; - if (timelineName == "inertia") - timeline = new PhysicsConstraintInertiaTimeline(frames, frames, constraintIndex); - else if (timelineName == "strength") - timeline = new PhysicsConstraintStrengthTimeline(frames, frames, constraintIndex); - else if (timelineName == "damping") - timeline = new PhysicsConstraintDampingTimeline(frames, frames, constraintIndex); - else if (timelineName == "mass") - timeline = new PhysicsConstraintMassTimeline(frames, frames, constraintIndex); - else if (timelineName == "wind") - timeline = new PhysicsConstraintWindTimeline(frames, frames, constraintIndex); - else if (timelineName == "gravity") - timeline = new PhysicsConstraintGravityTimeline(frames, frames, constraintIndex); - else if (timelineName == "mix") // - timeline = new PhysicsConstraintMixTimeline(frames, frames, constraintIndex); - else - continue; - timelines.push(readTimeline1(timelineMap, timeline, 0, 1)); + // Slider timelines. + if (map.slider) { + for (const constraintName in map.slider) { + const constraintMap = map.slider[constraintName]; + const constraint = skeletonData.findConstraint(constraintName, SliderData); + if (!constraint) throw new Error("Slider not found: " + constraintName); + const index = skeletonData.constraints.indexOf(constraint); + + for (const timelineName in constraintMap) { + const timelineMap = constraintMap[timelineName]; + let keyMap = timelineMap[0]; + if (!keyMap) continue; + + const frames = timelineMap.length; + switch (timelineName) { + case "time": readTimeline1(timelines, timelineMap, new SliderTimeline(frames, frames, index), 1, 1); break; + case "mix": readTimeline1(timelines, timelineMap, new SliderMixTimeline(frames, frames, index), 1, 1); break; + } } } } @@ -1024,6 +1100,7 @@ export class SkeletonJson { for (let attachmentMapName in slotMap) { let attachmentMap = slotMap[attachmentMapName]; let attachment = skin.getAttachment(slotIndex, attachmentMapName); + if (!attachment) throw new Error("Timeline attachment not found: " + attachmentMapName); for (let timelineMapName in attachmentMap) { let timelineMap = attachmentMap[timelineMapName]; @@ -1168,7 +1245,7 @@ class LinkedMesh { } } -function readTimeline1 (keys: any[], timeline: CurveTimeline1, defaultValue: number, scale: number) { +function readTimeline1 (timelines: Array, keys: any[], timeline: CurveTimeline1, defaultValue: number, scale: number) { let keyMap = keys[0]; let time = getValue(keyMap, "time", 0); let value = getValue(keyMap, "value", defaultValue) * scale; @@ -1178,7 +1255,8 @@ function readTimeline1 (keys: any[], timeline: CurveTimeline1, defaultValue: num let nextMap = keys[frame + 1]; if (!nextMap) { timeline.shrink(bezier); - return timeline; + timelines.push(timeline); + return; } let time2 = getValue(nextMap, "time", 0); let value2 = getValue(nextMap, "value", defaultValue) * scale; @@ -1189,7 +1267,7 @@ function readTimeline1 (keys: any[], timeline: CurveTimeline1, defaultValue: num } } -function readTimeline2 (keys: any[], timeline: CurveTimeline2, name1: string, name2: string, defaultValue: number, scale: number) { +function readTimeline2 (timelines: Array, keys: any[], timeline: BoneTimeline2, name1: string, name2: string, defaultValue: number, scale: number) { let keyMap = keys[0]; let time = getValue(keyMap, "time", 0); let value1 = getValue(keyMap, name1, defaultValue) * scale; @@ -1200,7 +1278,8 @@ function readTimeline2 (keys: any[], timeline: CurveTimeline2, name1: string, na let nextMap = keys[frame + 1]; if (!nextMap) { timeline.shrink(bezier); - return timeline; + timelines.push(timeline); + return; } let time2 = getValue(nextMap, "time", 0); let nvalue1 = getValue(nextMap, name1, defaultValue) * scale; diff --git a/spine-ts/spine-core/src/Skin.ts b/spine-ts/spine-core/src/Skin.ts index 8eb2d506a..517496622 100644 --- a/spine-ts/spine-core/src/Skin.ts +++ b/spine-ts/spine-core/src/Skin.ts @@ -49,7 +49,7 @@ export class Skin { attachments = new Array>(); bones = Array(); - constraints = new Array(); + constraints = new Array>(); /** The color of the skin as it was in Spine, or a default color if nonessential data was not exported. */ color = new Color(0.99607843, 0.61960787, 0.30980393, 1); // fe9e4fff @@ -192,14 +192,14 @@ export class Skin { let slotIndex = 0; for (let i = 0; i < skeleton.slots.length; i++) { let slot = skeleton.slots[i]; - let slotAttachment = slot.getAttachment(); + let slotAttachment = slot.pose.getAttachment(); if (slotAttachment && slotIndex < oldSkin.attachments.length) { let dictionary = oldSkin.attachments[slotIndex]; for (let key in dictionary) { let skinAttachment: Attachment = dictionary[key]; if (slotAttachment == skinAttachment) { let attachment = this.getAttachment(slotIndex, key); - if (attachment) slot.setAttachment(attachment); + 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 new file mode 100644 index 000000000..ed8b1a159 --- /dev/null +++ b/spine-ts/spine-core/src/Slider.ts @@ -0,0 +1,117 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { isConstraintTimeline, isSlotTimeline, MixBlend, MixDirection, PhysicsConstraintTimeline } from "./Animation"; +import { Bone } from "./Bone"; +import { Constraint } from "./Constraint"; +import { Physics } from "./Physics"; +import { Skeleton } from "./Skeleton"; +import { SliderData } from "./SliderData"; +import { SliderPose } from "./SliderPose"; + +/** Stores the setup pose for a {@link PhysicsConstraint}. + * + * See Physics constraints in the Spine User Guide. */ +export class Slider extends Constraint { + private static readonly offsets = new Array(); + + bone: Bone | null = null; + + constructor (data: SliderData, skeleton: Skeleton) { + super(data, new SliderPose(), new SliderPose()); + if (!skeleton) throw new Error("skeleton cannot be null."); + + if (data.bone != null) this.bone = skeleton.bones[data.bone.index]; + } + + public copy (skeleton: Skeleton) { + var copy = new Slider(this.data, skeleton); + copy.pose.set(this.pose); + return copy; + } + + public update (skeleton: Skeleton, physics: Physics) { + const p = this.applied; + 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); + p.time = (data.property.value(bone.applied, data.local, Slider.offsets) - data.property.offset) * data.scale; + if (data.loop) + p.time = animation.duration + (p.time % animation.duration); + else + p.time = Math.max(0, p.time); + } + + 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); + + animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, data.additive ? MixBlend.add : MixBlend.replace, + MixDirection.in, true); + } + + sort (skeleton: Skeleton) { + const bone = this.bone; + const data = this.data; + if (bone && data.local) skeleton.sortBone(bone); + skeleton._updateCache.push(this); + + const bones = skeleton.bones; + const indices = data.animation.bones; + for (let i = 0, n = data.animation.bones.length; i < n; i++) { + const bone = bones[indices[i]]; + bone.sorted = false; + skeleton.sortReset(bone.children); + skeleton.constrained(bone); + } + + const timelines = data.animation.timelines; + const slots = skeleton.slots; + const constraints = skeleton.constraints; + const physics = skeleton.physics; + const physicsCount = skeleton.physics.length; + for (let i = 0, n = data.animation.timelines.length; i < n; i++) { + const t = timelines[i]; + if (isSlotTimeline(t)) + skeleton.constrained(slots[t.slotIndex]); + else if (t instanceof PhysicsConstraintTimeline) { + if (t.constraintIndex == -1) { + for (let ii = 0; ii < physicsCount; ii++) + skeleton.constrained(physics[ii]); + } else + skeleton.constrained(constraints[t.constraintIndex]); + } else if (isConstraintTimeline(t)) // + skeleton.constrained(constraints[t.constraintIndex]); + } + } +} diff --git a/spine-ts/spine-core/src/SliderData.ts b/spine-ts/spine-core/src/SliderData.ts new file mode 100644 index 000000000..eac9c8f63 --- /dev/null +++ b/spine-ts/spine-core/src/SliderData.ts @@ -0,0 +1,57 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Animation } from "./Animation"; +import { BoneData } from "./BoneData"; +import { ConstraintData } from "./ConstraintData"; +import { Skeleton } from "./Skeleton"; +import { Slider } from "./Slider"; +import { SliderPose } from "./SliderPose"; +import { FromProperty } from "./TransformConstraintData"; + +/** Stores the setup pose for a {@link SliderConstraint}. + * + * See Slider constraints in the Spine User Guide. */ +export class SliderData extends ConstraintData { + animation!: Animation; + additive = false; + loop = false; + bone: BoneData | null = null; + property!: FromProperty; + scale = 0; + local = false; + + constructor (name: string) { + super(name, new SliderPose()); + } + + public create (skeleton: Skeleton) { + return new Slider(this, skeleton); + } +} diff --git a/spine-ts/spine-core/src/SliderPose.ts b/spine-ts/spine-core/src/SliderPose.ts new file mode 100644 index 000000000..46f854d24 --- /dev/null +++ b/spine-ts/spine-core/src/SliderPose.ts @@ -0,0 +1,41 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose"; + +/** Stores a pose for a slider. */ +export class SliderPose implements Pose { + time = 0; + mix = 0; + + set (pose: SliderPose) { + this.time = pose.time; + this.mix = pose.mix; + } +} diff --git a/spine-ts/spine-core/src/Slot.ts b/spine-ts/spine-core/src/Slot.ts index 041001029..716bf2c1d 100644 --- a/spine-ts/spine-core/src/Slot.ts +++ b/spine-ts/spine-core/src/Slot.ts @@ -27,86 +27,45 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Attachment, VertexAttachment } from "./attachments/Attachment.js"; import { Bone } from "./Bone.js"; +import { Posed } from "./Posed.js"; import { Skeleton } from "./Skeleton.js"; import { 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 { - /** The slot's setup pose data. */ - data: SlotData; +export class Slot extends Posed { + readonly skeleton: Skeleton; /** The bone this slot belongs to. */ - bone: Bone; - - /** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two - * color tinting. */ - color: Color; - - /** 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 - * color's alpha is not used. */ - darkColor: Color | null = null; - - attachment: Attachment | null = null; + readonly bone: Bone; attachmentState: number = 0; - /** The index of the texture region to display when the slot's attachment has a {@link Sequence}. -1 represents the - * {@link Sequence#getSetupIndex()}. */ - sequenceIndex: number = -1; - - /** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a - * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. - * - * See {@link VertexAttachment#computeWorldVertices()} and {@link DeformTimeline}. */ - deform = new Array(); - - constructor (data: SlotData, bone: Bone) { - if (!data) throw new Error("data cannot be null."); - if (!bone) throw new Error("bone cannot be null."); - this.data = data; - this.bone = bone; - this.color = new Color(); - this.darkColor = !data.darkColor ? null : new Color(); - this.setToSetupPose(); - } - - /** The skeleton this slot belongs to. */ - getSkeleton (): Skeleton { - return this.bone.skeleton; - } - - /** The current attachment for the slot, or null if the slot has no attachment. */ - getAttachment (): Attachment | null { - return this.attachment; - } - - /** Sets the slot's attachment and, if the attachment changed, resets {@link #sequenceIndex} and clears the {@link #deform}. - * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getTimelineAttachment()} as the - * specified attachment. */ - setAttachment (attachment: Attachment | null) { - if (this.attachment == attachment) return; - if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment) - || attachment.timelineAttachment != this.attachment.timelineAttachment) { - this.deform.length = 0; + constructor (data: SlotData, skeleton: Skeleton) { + super(data, new SlotPose(), new SlotPose()); + if (!skeleton) throw new Error("skeleton cannot be null."); + this.skeleton = skeleton; + this.bone = skeleton.bones[data.boneData.index]; + if (data.setup.darkColor != null) { + this.pose.darkColor = new Color(); + this.constrained.darkColor = new Color(); } - this.attachment = attachment; - this.sequenceIndex = -1; + this.setupPose(); } - /** Sets this slot to the setup pose. */ - setToSetupPose () { - this.color.setFromColor(this.data.color); - if (this.darkColor) this.darkColor.setFromColor(this.data.darkColor!); + setupPose () { + this.pose.color.setFromColor(this.data.setup.color); + if (this.pose.darkColor) this.pose.darkColor.setFromColor(this.data.setup.darkColor!); + this.pose.sequenceIndex = this.data.setup.sequenceIndex; if (!this.data.attachmentName) - this.attachment = null; + this.pose.setAttachment(null); else { - this.attachment = null; - this.setAttachment(this.bone.skeleton.getAttachment(this.data.index, this.data.attachmentName)); + this.pose.attachment = null; + this.pose.setAttachment(this.skeleton.getAttachment(this.data.index, this.data.attachmentName)); } } } diff --git a/spine-ts/spine-core/src/SlotData.ts b/spine-ts/spine-core/src/SlotData.ts index 0947170cb..6ea7d07f6 100644 --- a/spine-ts/spine-core/src/SlotData.ts +++ b/spine-ts/spine-core/src/SlotData.ts @@ -28,42 +28,34 @@ *****************************************************************************/ import { BoneData } from "./BoneData.js"; -import { Color } from "./Utils.js"; +import { PosedData } from "./PosedData.js"; +import { SlotPose } from "./SlotPose.js"; + +import type { Skeleton } from "./Skeleton.js"; /** Stores the setup pose for a {@link Slot}. */ -export class SlotData { - /** The index of the slot in {@link Skeleton#getSlots()}. */ +export class SlotData extends PosedData { + /** The index of the slot in {@link Skeleton.getSlots()}. */ index: number = 0; - /** The name of the slot, which is unique across all slots in the skeleton. */ - name: string; - /** The bone this slot belongs to. */ boneData: BoneData; - /** The color used to tint the slot's attachment. If {@link #getDarkColor()} is set, this is used as the light color for two - * color tinting. */ - 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 - * color's alpha is not used. */ - darkColor: Color | null = null; - /** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */ attachmentName: string | null = null; /** The blend mode for drawing the slot's attachment. */ blendMode: BlendMode = BlendMode.Normal; + // Nonessential. /** False if the slot was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */ visible = true; constructor (index: number, name: string, boneData: BoneData) { + super(name, new SlotPose()); if (index < 0) throw new Error("index must be >= 0."); - if (!name) throw new Error("name cannot be null."); if (!boneData) throw new Error("boneData cannot be null."); this.index = index; - this.name = name; this.boneData = boneData; } } diff --git a/spine-ts/spine-core/src/SlotPose.ts b/spine-ts/spine-core/src/SlotPose.ts new file mode 100644 index 000000000..7cc4add4c --- /dev/null +++ b/spine-ts/spine-core/src/SlotPose.ts @@ -0,0 +1,93 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose"; +import { Color } from "./Utils"; + +import { VertexAttachment } from "./attachments/Attachment"; +import { Attachment } from "./attachments/Attachment"; +import type { Sequence } from "./attachments/Sequence"; + +/** 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. */ +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. */ + 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 + * color's alpha is not used. */ + darkColor: Color | null = null; + + /** The current attachment for the slot, or null if the slot has no attachment. */ + attachment: Attachment | null = null; // Not used in setup pose. + + /** The index of the texture region to display when the slot's attachment has a {@link Sequence}. -1 represents the + * {@link Sequence.getSetupIndex()}. */ + sequenceIndex = 0; + + /** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a + * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions. + * + * See {@link VertexAttachment.computeWorldVertices()} and + * {@link DeformTimeline}. */ + readonly deform = new Array(); + + SlotPose () { + } + + public set (pose: SlotPose): void { + if (pose == null) throw new Error("pose cannot be null."); + this.color.setFromColor(pose.color); + if (this.darkColor != null && pose.darkColor != null) this.darkColor.setFromColor(pose.darkColor); + this.attachment = pose.attachment; + this.sequenceIndex = pose.sequenceIndex; + this.deform.length = 0; + this.deform.push(...pose.deform); + } + + /** The current attachment for the slot, or null if the slot has no attachment. */ + getAttachment (): Attachment | null { + return this.attachment; + } + + /** Sets the slot's attachment and, if the attachment changed, resets {@link #sequenceIndex} and clears the {@link #deform}. + * The deform is not cleared if the old attachment has the same {@link VertexAttachment.getTimelineAttachment()} as the + * specified attachment. */ + setAttachment (attachment: Attachment | null): void { + if (this.attachment == attachment) return; + if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment) + || attachment.timelineAttachment != this.attachment.timelineAttachment) { + this.deform.length = 0; + } + this.attachment = attachment; + this.sequenceIndex = -1; + } +} diff --git a/spine-ts/spine-core/src/TransformConstraint.ts b/spine-ts/spine-core/src/TransformConstraint.ts index 0d5fc36b6..2eedd94fc 100644 --- a/spine-ts/spine-core/src/TransformConstraint.ts +++ b/spine-ts/spine-core/src/TransformConstraint.ts @@ -28,87 +28,71 @@ *****************************************************************************/ import { Bone } from "./Bone.js"; -import { Physics, Skeleton } from "./Skeleton.js"; +import { BonePose } from "./BonePose.js"; +import { Constraint } from "./Constraint.js"; +import { Physics } from "./Physics.js"; +import { Skeleton } from "./Skeleton.js"; import { TransformConstraintData } from "./TransformConstraintData.js"; -import { Updatable } from "./Updatable.js"; -import { Vector2, MathUtils } from "./Utils.js"; +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. * * See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */ -export class TransformConstraint implements Updatable { - - /** The transform constraint's setup pose data. */ - data: TransformConstraintData; +export class TransformConstraint extends Constraint { /** The bones that will be modified by this transform constraint. */ - bones: Array; + bones: Array; /** The bone whose world transform will be copied to the constrained bones. */ source: Bone; - mixRotate = 0; mixX = 0; mixY = 0; mixScaleX = 0; mixScaleY = 0; mixShearY = 0; - - temp = new Vector2(); - active = false; - constructor (data: TransformConstraintData, skeleton: Skeleton) { - if (!data) throw new Error("data cannot be null."); + super(data, new TransformConstraintPose(), new TransformConstraintPose()); if (!skeleton) throw new Error("skeleton cannot be null."); - this.data = data; - this.bones = new Array(); - for (let i = 0; i < data.bones.length; i++) { - let bone = skeleton.findBone(data.bones[i].name); - if (!bone) throw new Error(`Couldn't find bone ${data.bones[i].name}.`); - this.bones.push(bone); - } - let target = skeleton.findBone(data.source.name); - if (!target) throw new Error(`Couldn't find target bone ${data.source.name}.`); - this.source = target; + this.bones = new Array(); + for (const boneData of data.bones) + this.bones.push(skeleton.bones[boneData.index].constrained); - this.mixRotate = data.mixRotate; - this.mixX = data.mixX; - this.mixY = data.mixY; - this.mixScaleX = data.mixScaleX; - this.mixScaleY = data.mixScaleY; - this.mixShearY = data.mixShearY; + const source = skeleton.bones[data.source.index]; + if (source == null) throw new Error("source cannot be null."); + this.source = source; } - isActive () { - return this.active; + public copy (skeleton: Skeleton) { + var copy = new TransformConstraint(this.data, skeleton); + copy.pose.set(this.pose); + return copy; } - setToSetupPose () { - const data = this.data; - this.mixRotate = data.mixRotate; - this.mixX = data.mixX; - this.mixY = data.mixY; - this.mixScaleX = data.mixScaleX; - this.mixScaleY = data.mixScaleY; - this.mixShearY = data.mixShearY; - } - - update (physics: Physics) { - if (this.mixRotate == 0 && this.mixX == 0 && this.mixY == 0 && this.mixScaleX == 0 && this.mixScaleY == 0 && this.mixShearY == 0) return; + update (skeleton: Skeleton, physics: Physics) { + const p = this.applied; + if (p.mixRotate == 0 && p.mixX == 0 && p.mixY == 0 && p.mixScaleX == 0 && p.mixScaleY == 0 && p.mixShearY == 0) return; const data = this.data; - const localFrom = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp; - const source = this.source; + const localSource = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp; + const offsets = data.offsets; + const source = this.source.applied; + if (localSource) source.validateLocalTransform(skeleton); const fromItems = data.properties; - const fn = data.properties.length; + const fn = data.properties.length, update = skeleton._update; const bones = this.bones; for (let i = 0, n = this.bones.length; i < n; i++) { const bone = bones[i]; + if (localTarget) + bone.modifyLocal(skeleton); + else + bone.modifyWorld(update); for (let f = 0; f < fn; f++) { const from = fromItems[f]; - const value = from.value(data, source, localFrom) - from.offset; + const value = from.value(source, localSource, offsets) - from.offset; const toItems = from.to; for (let t = 0, tn = from.to.length; t < tn; t++) { - var to = toItems[t]; - if (to.mix(this) != 0) { + const to = toItems[t]; + if (to.mix(p) !== 0) { let clamped = to.offset + value * to.scale; if (clamp) { if (to.offset < to.max) @@ -116,14 +100,34 @@ export class TransformConstraint implements Updatable { else clamped = MathUtils.clamp(clamped, to.max, to.offset); } - to.apply(this, bone, clamped, localTarget, additive); + to.apply(p, bone, clamped, localTarget, additive); } } } - if (localTarget) - bone.update(null); - else - bone.updateAppliedTransform(); } } + + sort (skeleton: Skeleton) { + if (!this.data.localSource) skeleton.sortBone(this.source); + const bones = this.bones; + const boneCount = this.bones.length; + const worldTarget = !this.data.localTarget; + if (worldTarget) { + for (let i = 0; i < boneCount; i++) + skeleton.sortBone(bones[i].bone); + } + skeleton._updateCache.push(this); + for (let i = 0; i < boneCount; i++) { + const bone = bones[i].bone; + skeleton.sortReset(bone.children); + skeleton.constrained(bone); + } + for (let i = 0; i < boneCount; i++) + bones[i].bone.sorted = worldTarget; + } + + isSourceActive () { + return this.source.active; + } + } diff --git a/spine-ts/spine-core/src/TransformConstraintData.ts b/spine-ts/spine-core/src/TransformConstraintData.ts index 03c91e117..b4c572d10 100644 --- a/spine-ts/spine-core/src/TransformConstraintData.ts +++ b/spine-ts/spine-core/src/TransformConstraintData.ts @@ -29,25 +29,28 @@ import { ConstraintData } from "./ConstraintData.js"; import { BoneData } from "./BoneData.js"; -import { Bone } from "./Bone.js"; import { TransformConstraint } from "./TransformConstraint.js"; import { MathUtils } from "./Utils.js"; +import { Skeleton } from "./Skeleton.js"; +import { TransformConstraintPose } from "./TransformConstraintPose.js"; +import { BonePose } from "./BonePose.js"; /** Stores the setup pose for a {@link TransformConstraint}. * * See [Transform constraints](http://esotericsoftware.com/spine-transform-constraints) in the Spine User Guide. */ -export class TransformConstraintData extends ConstraintData { - +export class TransformConstraintData extends ConstraintData { /** The bones that will be modified by this transform constraint. */ bones = new Array(); /** The bone whose world transform will be copied to the constrained bones. */ - private _source: BoneData | null = null; public set source (source: BoneData) { this._source = source; } public get source () { if (!this._source) throw new Error("BoneData not set.") - else return this._source; + else return this._source; } + private _source: BoneData | null = null; + + offsets = new Array(); /** An offset added to the constrained bone X translation. */ offsetX = 0; @@ -55,24 +58,6 @@ export class TransformConstraintData extends ConstraintData { /** An offset added to the constrained bone Y translation. */ offsetY = 0; - /** A percentage (0-1) 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. */ - mixX = 0; - - /** A percentage (0-1) 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. */ - mixScaleX = 0; - - /** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */ - mixScaleY = 0; - - /** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */ - mixShearY = 0; - /** Reads the source bone's local transform instead of its world transform. */ localSource = false; @@ -82,14 +67,72 @@ export class TransformConstraintData extends ConstraintData { /** Adds the source bone transform to the constrained bones instead of setting it absolutely. */ additive = false; - /** Prevents constrained bones from exceeding the ranged defined by {@link ToProperty#offset} and {@link ToProperty#max}. */ + /** Prevents constrained bones from exceeding the ranged defined by {@link ToProperty.offset} and {@link ToProperty.max}. */ clamp = false; /** The mapping of transform properties to other transform properties. */ readonly properties: Array = []; constructor (name: string) { - super(name, 0, false); + super(name, new TransformConstraintPose()); + } + + public create (skeleton: Skeleton) { + return new TransformConstraint(this, skeleton); + } + + /** An offset added to the constrained bone rotation. */ + getOffsetRotation () { + return this.offsets[0]; + } + + setOffsetRotation (offsetRotation: number) { + this.offsets[0] = offsetRotation; + } + + /** An offset added to the constrained bone X translation. */ + getOffsetX () { + return this.offsets[1]; + } + + setOffsetX (offsetX: number) { + this.offsets[1] = offsetX; + } + + /** An offset added to the constrained bone Y translation. */ + getOffsetY () { + return this.offsets[2]; + } + + setOffsetY (offsetY: number) { + this.offsets[2] = offsetY; + } + + /** An offset added to the constrained bone scaleX. */ + getOffsetScaleX () { + return this.offsets[3]; + } + + setOffsetScaleX (offsetScaleX: number) { + this.offsets[3] = offsetScaleX; + } + + /** An offset added to the constrained bone scaleY. */ + getOffsetScaleY () { + return this.offsets[4]; + } + + setOffsetScaleY (offsetScaleY: number) { + this.offsets[4] = offsetScaleY; + } + + /** An offset added to the constrained bone shearY. */ + getOffsetShearY () { + return this.offsets[5]; + } + + setOffsetShearY (offsetShearY: number) { + this.offsets[5] = offsetShearY; } } @@ -103,7 +146,7 @@ export abstract class FromProperty { readonly to: Array = []; /** Reads this property from the specified bone. */ - abstract value (data: TransformConstraintData, source: Bone, local: boolean): number; + abstract value (source: BonePose, local: boolean, offsets: Array): number; } /** Constrained property for a {@link TransformConstraint}. */ @@ -118,27 +161,31 @@ export abstract class ToProperty { scale = 0; /** Reads the mix for this property from the specified constraint. */ - abstract mix (constraint: TransformConstraint): number; + abstract mix (pose: TransformConstraintPose): number; /** Applies the value to this property. */ - abstract apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void; + abstract apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void; } export class FromRotate extends FromProperty { - value (data: TransformConstraintData, source: Bone, local: boolean): number { - return local ? source.arotation : Math.atan2(source.c, source.a) * MathUtils.radDeg; + value (source: BonePose, local: boolean, offsets: Array): number { + if (local) return source.rotation + offsets[0]; + let value = Math.atan2(source.c, source.a) * MathUtils.radDeg + + (source.a * source.d - source.b * source.c > 0 ? offsets[0] : -offsets[0]); + if (value < 0) value += 360; + return value; } } export class ToRotate extends ToProperty { - mix (constraint: TransformConstraint): number { - return constraint.mixRotate; + mix (pose: TransformConstraintPose): number { + return pose.mixRotate; } - apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void { + apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void { if (local) { - if (!additive) value -= bone.arotation; - bone.arotation += value * constraint.mixRotate; + if (!additive) value -= bone.rotation; + bone.rotation += value * pose.mixRotate; } else { const a = bone.a, b = bone.b, c = bone.c, d = bone.d; value *= MathUtils.degRad; @@ -147,7 +194,7 @@ export class ToRotate extends ToProperty { value -= MathUtils.PI2; else if (value < -MathUtils.PI) // value += MathUtils.PI2; - value *= constraint.mixRotate; + value *= pose.mixRotate; const cos = Math.cos(value), sin = Math.sin(value); bone.a = cos * a - sin * c; bone.b = cos * b - sin * d; @@ -158,73 +205,73 @@ export class ToRotate extends ToProperty { } export class FromX extends FromProperty { - value (data: TransformConstraintData, source: Bone, local: boolean): number { - return local ? source.ax + data.offsetX : data.offsetX * source.a + data.offsetY * source.b + source.worldX; + value (source: BonePose, local: boolean, offsets: Array): number { + return local ? source.x + offsets[1] : offsets[1] * source.a + offsets[2] * source.b + source.worldX; } } export class ToX extends ToProperty { - mix (constraint: TransformConstraint): number { - return constraint.mixX; + mix (pose: TransformConstraintPose): number { + return pose.mixX; } - apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void { + apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void { if (local) { - if (!additive) value -= bone.ax; - bone.ax += value * constraint.mixX; + if (!additive) value -= bone.x; + bone.x += value * pose.mixX; } else { if (!additive) value -= bone.worldX; - bone.worldX += value * constraint.mixX; + bone.worldX += value * pose.mixX; } } } export class FromY extends FromProperty { - value (data: TransformConstraintData, source: Bone, local: boolean): number { - return local ? source.ay + data.offsetY : data.offsetX * source.c + data.offsetY * source.d + source.worldY; + value (source: BonePose, local: boolean, offsets: Array): number { + return local ? source.y + offsets[2] : offsets[1] * source.c + offsets[2] * source.d + source.worldY; } } export class ToY extends ToProperty { - mix (constraint: TransformConstraint): number { - return constraint.mixY; + mix (pose: TransformConstraintPose): number { + return pose.mixY; } - apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void { + apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void { if (local) { - if (!additive) value -= bone.ay; - bone.ay += value * constraint.mixY; + if (!additive) value -= bone.y; + bone.y += value * pose.mixY; } else { if (!additive) value -= bone.worldY; - bone.worldY += value * constraint.mixY; + bone.worldY += value * pose.mixY; } } } export class FromScaleX extends FromProperty { - value (data: TransformConstraintData, source: Bone, local: boolean): number { - return local ? source.ascaleX : Math.sqrt(source.a * source.a + source.c * source.c); + value (source: BonePose, local: boolean, offsets: Array): number { + return local ? source.scaleX : Math.sqrt(source.a * source.a + source.c * source.c) + offsets[3]; } } export class ToScaleX extends ToProperty { - mix (constraint: TransformConstraint): number { - return constraint.mixScaleX; + mix (pose: TransformConstraintPose): number { + return pose.mixScaleX; } - apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void { + apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void { if (local) { if (additive) - bone.ascaleX *= 1 + ((value - 1) * constraint.mixScaleX); - else if (bone.ascaleX != 0) - bone.ascaleX = 1 + (value / bone.ascaleX - 1) * constraint.mixScaleX; + bone.scaleX *= 1 + ((value - 1) * pose.mixScaleX); + else if (bone.scaleX != 0) // + bone.scaleX = 1 + (value / bone.scaleX - 1) * pose.mixScaleX; } else { let s: number; if (additive) - s = 1 + (value - 1) * constraint.mixScaleX; + s = 1 + (value - 1) * pose.mixScaleX; else { s = Math.sqrt(bone.a * bone.a + bone.c * bone.c); - if (s != 0) s = 1 + (value / s - 1) * constraint.mixScaleX; + if (s != 0) s = 1 + (value / s - 1) * pose.mixScaleX; } bone.a *= s; bone.c *= s; @@ -233,29 +280,29 @@ export class ToScaleX extends ToProperty { } export class FromScaleY extends FromProperty { - value (data: TransformConstraintData, source: Bone, local: boolean): number { - return local ? source.ascaleY : Math.sqrt(source.b * source.b + source.d * source.d); + value (source: BonePose, local: boolean, offsets: Array): number { + return local ? source.scaleY : Math.sqrt(source.b * source.b + source.d * source.d) + offsets[4]; } } export class ToScaleY extends ToProperty { - mix (constraint: TransformConstraint): number { - return constraint.mixScaleY; + mix (pose: TransformConstraintPose): number { + return pose.mixScaleY; } - apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void { + apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void { if (local) { if (additive) - bone.ascaleY *= 1 + ((value - 1) * constraint.mixScaleY); - else if (bone.ascaleY != 0) // - bone.ascaleY = 1 + (value / bone.ascaleY - 1) * constraint.mixScaleY; + bone.scaleY *= 1 + ((value - 1) * pose.mixScaleY); + else if (bone.scaleY != 0) // + bone.scaleY = 1 + (value / bone.scaleY - 1) * pose.mixScaleY; } else { let s: number; if (additive) - s = 1 + (value - 1) * constraint.mixScaleY; + s = 1 + (value - 1) * pose.mixScaleY; else { s = Math.sqrt(bone.b * bone.b + bone.d * bone.d); - if (s != 0) s = 1 + (value / s - 1) * constraint.mixScaleY; + if (s != 0) s = 1 + (value / s - 1) * pose.mixScaleY; } bone.b *= s; bone.d *= s; @@ -264,20 +311,20 @@ export class ToScaleY extends ToProperty { } export class FromShearY extends FromProperty { - value (data: TransformConstraintData, source: Bone, local: boolean): number { - return local ? source.ashearY : (Math.atan2(source.d, source.b) - Math.atan2(source.c, source.a)) * MathUtils.radDeg - 90; + value (source: BonePose, local: boolean, offsets: Array): number { + return (local ? source.shearY : (Math.atan2(source.d, source.b) - Math.atan2(source.c, source.a)) * MathUtils.radDeg - 90) + offsets[5]; } } export class ToShearY extends ToProperty { - mix (constraint: TransformConstraint): number { - return constraint.mixShearY; + mix (pose: TransformConstraintPose): number { + return pose.mixShearY; } - apply (constraint: TransformConstraint, bone: Bone, value: number, local: boolean, additive: boolean): void { + apply (pose: TransformConstraintPose, bone: BonePose, value: number, local: boolean, additive: boolean): void { if (local) { - if (!additive) value -= bone.ashearY; - bone.ashearY += value * constraint.mixShearY; + if (!additive) value -= bone.shearY; + bone.shearY += value * pose.mixShearY; } else { const b = bone.b, d = bone.d, by = Math.atan2(d, b); value = (value + 90) * MathUtils.degRad; @@ -290,7 +337,7 @@ export class ToShearY extends ToProperty { else if (value < -MathUtils.PI) value += MathUtils.PI2; } - value = by + value * constraint.mixShearY; + value = by + value * pose.mixShearY; const s = Math.sqrt(b * b + d * d); bone.b = Math.cos(value) * s; bone.d = Math.sin(value) * s; diff --git a/spine-ts/spine-core/src/TransformConstraintPose.ts b/spine-ts/spine-core/src/TransformConstraintPose.ts new file mode 100644 index 000000000..f8167a1cd --- /dev/null +++ b/spine-ts/spine-core/src/TransformConstraintPose.ts @@ -0,0 +1,60 @@ +/****************************************************************************** + * Spine Runtimes License Agreement + * Last updated April 5, 2025. Replaces all prior versions. + * + * Copyright (c) 2013-2025, Esoteric Software LLC + * + * Integration of the Spine Runtimes into software or otherwise creating + * derivative works of the Spine Runtimes is permitted under the terms and + * conditions of Section 2 of the Spine Editor License Agreement: + * http://esotericsoftware.com/spine-editor-license + * + * Otherwise, it is permitted to integrate the Spine Runtimes into software + * or otherwise create derivative works of the Spine Runtimes (collectively, + * "Products"), provided that each user of the Products must obtain their own + * Spine Editor license and redistribution of the Products in any form must + * include this license and copyright notice. + * + * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, + * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + *****************************************************************************/ + +import { Pose } from "./Pose" + +/** Stores a pose for a transform constraint. */ +export class TransformConstraintPose implements Pose { + /** A percentage (0-1) 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. */ + mixX = 0; + + /** A percentage (0-1) 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. */ + mixScaleX = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */ + mixScaleY = 0; + + /** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */ + mixShearY = 0; + + public set (pose: TransformConstraintPose) { + this.mixRotate = pose.mixRotate; + this.mixX = pose.mixX; + this.mixY = pose.mixY; + this.mixScaleX = pose.mixScaleX; + this.mixScaleY = pose.mixScaleY; + this.mixShearY = pose.mixShearY; + } +} diff --git a/spine-ts/spine-core/src/Updatable.ts b/spine-ts/spine-core/src/Update.ts similarity index 80% rename from spine-ts/spine-core/src/Updatable.ts rename to spine-ts/spine-core/src/Update.ts index 93c37ce4b..cd7c594c4 100644 --- a/spine-ts/spine-core/src/Updatable.ts +++ b/spine-ts/spine-core/src/Update.ts @@ -27,19 +27,11 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Physics } from "./Skeleton.js"; +import type { Physics } from "./Physics.js"; +import type { Skeleton } from "./Skeleton.js"; /** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */ -export interface Updatable { +export interface Update { /** @param physics Determines how physics and other non-deterministic updates are applied. */ - update (physics: Physics): void; - - /** Returns false when this item 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 BoneData#getSkinRequired() - * @see ConstraintData#getSkinRequired() */ - isActive (): boolean; + update (skeleton: Skeleton, physics: Physics): void; } diff --git a/spine-ts/spine-core/src/Utils.ts b/spine-ts/spine-core/src/Utils.ts index 8d0589e42..e9255fc59 100644 --- a/spine-ts/spine-core/src/Utils.ts +++ b/spine-ts/spine-core/src/Utils.ts @@ -203,7 +203,7 @@ export class MathUtils { } static atan2Deg (y: number, x: number) { - return Math.atan2(y, x) * MathUtils.degRad; + return Math.atan2(y, x) * MathUtils.radDeg; } static signum (value: number): number { @@ -347,8 +347,8 @@ export class Utils { export class DebugUtils { static logBones (skeleton: Skeleton) { for (let i = 0; i < skeleton.bones.length; i++) { - let bone = skeleton.bones[i]; - console.log(bone.data.name + ", " + bone.a + ", " + bone.b + ", " + bone.c + ", " + bone.d + ", " + bone.worldX + ", " + bone.worldY); + let bone = skeleton.bones[i].applied; + 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 ffff260da..f884dc4dd 100644 --- a/spine-ts/spine-core/src/attachments/Attachment.ts +++ b/spine-ts/spine-core/src/attachments/Attachment.ts @@ -27,6 +27,7 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ +import { Skeleton } from "src/Skeleton.js"; import { Slot } from "../Slot.js"; import { NumberArrayLike, Utils } from "../Utils.js"; @@ -43,15 +44,15 @@ export abstract class Attachment { } /** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's - * {@link Slot#deform}. */ + * {@link SlotPose.deform}. */ export abstract class VertexAttachment extends Attachment { private static nextID = 0; /** The unique ID for this attachment. */ id = VertexAttachment.nextID++; - /** The bones which affect the {@link #getVertices()}. 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 + /** 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. */ bones: Array | null = null; @@ -61,7 +62,7 @@ export abstract class VertexAttachment extends Attachment { vertices: NumberArrayLike = []; /** The maximum number of world vertex values that can be output by - * {@link #computeWorldVertices()} using the `count` parameter. */ + * {@link computeWorldVertices} using the `count` parameter. */ worldVerticesLength = 0; /** Timelines for the timeline attachment are also applied to this attachment. @@ -72,7 +73,7 @@ export abstract class VertexAttachment extends Attachment { super(name); } - /** Transforms the attachment's local {@link #vertices} to world coordinates. If the slot's {@link Slot#deform} is + /** 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 @@ -83,15 +84,16 @@ export abstract class VertexAttachment extends Attachment { * `stride` / 2. * @param offset The `worldVertices` index to begin writing values. * @param stride The number of `worldVertices` entries between the value pairs written. */ - computeWorldVertices (slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) { + computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, + stride: number) { + count = offset + (count >> 1) * stride; - let skeleton = slot.bone.skeleton; - let deformArray = slot.deform; + let deformArray = slot.applied.deform; let vertices = this.vertices; let bones = this.bones; if (!bones) { if (deformArray.length > 0) vertices = deformArray; - let bone = slot.bone; + let bone = slot.bone.applied; let x = bone.worldX; let y = bone.worldY; let a = bone.a, b = bone.b, c = bone.c, d = bone.d; @@ -115,7 +117,7 @@ export abstract class VertexAttachment extends Attachment { let n = bones[v++]; n += v; for (; v < n; v++, b += 3) { - let bone = skeletonBones[bones[v]]; + let bone = skeletonBones[bones[v]].applied; let 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; @@ -130,7 +132,7 @@ export abstract class VertexAttachment extends Attachment { let n = bones[v++]; n += v; for (; v < n; v++, b += 3, f += 2) { - let bone = skeletonBones[bones[v]]; + let bone = skeletonBones[bones[v]].applied; let 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/MeshAttachment.ts b/spine-ts/spine-core/src/attachments/MeshAttachment.ts index ed40d5d45..d65647f9d 100644 --- a/spine-ts/spine-core/src/attachments/MeshAttachment.ts +++ b/spine-ts/spine-core/src/attachments/MeshAttachment.ts @@ -34,6 +34,7 @@ import { VertexAttachment, Attachment } from "./Attachment.js"; import { HasTextureRegion } from "./HasTextureRegion.js"; import { Sequence } from "./Sequence.js"; import { Slot } from "../Slot.js"; +import { Skeleton } from "src/Skeleton.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. @@ -126,11 +127,12 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion uvs[i + 1] = v + regionUVs[i] * height; } return; + default: + u -= region.offsetX / textureWidth; + v -= (region.originalHeight - region.offsetY - region.height) / textureHeight; + width = region.originalWidth / textureWidth; + height = region.originalHeight / textureHeight; } - u -= region.offsetX / textureWidth; - v -= (region.originalHeight - region.offsetY - region.height) / textureHeight; - width = region.originalWidth / textureWidth; - height = region.originalHeight / textureHeight; } else if (!this.region) { u = v = 0; width = height = 1; @@ -195,9 +197,9 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion return copy; } - computeWorldVertices (slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) { - if (this.sequence != null) this.sequence.apply(slot, this); - super.computeWorldVertices(slot, start, count, worldVertices, offset, stride); + computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) { + if (this.sequence != null) this.sequence.apply(slot.applied, this); + super.computeWorldVertices(skeleton, slot, start, count, worldVertices, offset, stride); } /** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/ diff --git a/spine-ts/spine-core/src/attachments/PointAttachment.ts b/spine-ts/spine-core/src/attachments/PointAttachment.ts index 86f92bb40..fffac9060 100644 --- a/spine-ts/spine-core/src/attachments/PointAttachment.ts +++ b/spine-ts/spine-core/src/attachments/PointAttachment.ts @@ -27,7 +27,7 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Bone } from "../Bone.js"; +import { BonePose } from "src/BonePose.js"; import { Color, Vector2, MathUtils } from "../Utils.js"; import { VertexAttachment, Attachment } from "./Attachment.js"; @@ -49,17 +49,17 @@ export class PointAttachment extends VertexAttachment { super(name); } - computeWorldPosition (bone: Bone, point: Vector2) { + 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; } - computeWorldRotation (bone: Bone) { + 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; const y = cos * bone.c + sin * bone.d; - return MathUtils.atan2Deg(y, x); + return MathUtils.atan2Deg(y, x); } copy (): Attachment { diff --git a/spine-ts/spine-core/src/attachments/RegionAttachment.ts b/spine-ts/spine-core/src/attachments/RegionAttachment.ts index ef552d5f9..8b2a4780a 100644 --- a/spine-ts/spine-core/src/attachments/RegionAttachment.ts +++ b/spine-ts/spine-core/src/attachments/RegionAttachment.ts @@ -27,7 +27,6 @@ * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ -import { Bone } from "../Bone.js"; import { TextureRegion } from "../Texture.js"; import { Color, MathUtils, NumberArrayLike, Utils } from "../Utils.js"; import { Attachment } from "./Attachment.js"; @@ -159,10 +158,9 @@ export class RegionAttachment extends Attachment implements HasTextureRegion { * @param offset The worldVertices index to begin writing values. * @param stride The number of worldVertices entries between the value pairs written. */ computeWorldVertices (slot: Slot, worldVertices: NumberArrayLike, offset: number, stride: number) { - if (this.sequence != null) - this.sequence.apply(slot, this); + if (this.sequence) this.sequence.apply(slot.applied, this); - let bone = slot.bone; + let bone = slot.bone.applied; let vertexOffset = this.offset; let x = bone.worldX, y = bone.worldY; let 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 2226641ea..6387bd873 100644 --- a/spine-ts/spine-core/src/attachments/Sequence.ts +++ b/spine-ts/spine-core/src/attachments/Sequence.ts @@ -28,9 +28,9 @@ *****************************************************************************/ import { TextureRegion } from "../Texture.js"; -import { Slot } from "../Slot.js"; import { HasTextureRegion } from "./HasTextureRegion.js"; import { Utils } from "../Utils.js"; +import { SlotPose } from "src/SlotPose.js"; export class Sequence { @@ -56,7 +56,7 @@ export class Sequence { return copy; } - apply (slot: Slot, attachment: HasTextureRegion) { + apply (slot: SlotPose, attachment: HasTextureRegion) { let index = slot.sequenceIndex; if (index == -1) index = this.setupIndex; if (index >= this.regions.length) index = this.regions.length - 1; diff --git a/spine-ts/spine-core/src/index.ts b/spine-ts/spine-core/src/index.ts index f656baa4c..a7f9a8752 100644 --- a/spine-ts/spine-core/src/index.ts +++ b/spine-ts/spine-core/src/index.ts @@ -2,16 +2,29 @@ export * from './Animation.js'; export * from './AnimationState.js'; export * from './AnimationStateData.js'; export * from './AtlasAttachmentLoader.js'; +export * from './AssetManagerBase.js'; export * from './Bone.js'; export * from './BoneData.js'; +export * from './BoneLocal.js'; +export * from './BonePose.js'; +export * from './Constraint.js'; export * from './ConstraintData.js'; -export * from './AssetManagerBase.js'; export * from './Event.js'; export * from './EventData.js'; export * from './IkConstraint.js'; export * from './IkConstraintData.js'; +export * from './IkConstraintPose.js'; export * from './PathConstraint.js'; export * from './PathConstraintData.js'; +export * from './PathConstraintPose.js'; +export * from './Physics.js'; +export * from './PhysicsConstraint.js'; +export * from './PhysicsConstraintData.js'; +export * from './PhysicsConstraintPose.js'; +export * from './Pose.js'; +export * from './Posed.js'; +export * from './PosedActive.js'; +export * from './PosedData.js'; export * from './Skeleton.js'; export * from './SkeletonBinary.js'; export * from './SkeletonBounds.js'; @@ -19,21 +32,28 @@ export * from './SkeletonClipping.js'; export * from './SkeletonData.js'; export * from './SkeletonJson.js'; export * from './Skin.js'; +export * from './Slider.js'; +export * from './SliderData.js'; +export * from './SliderPose.js'; export * from './Slot.js'; export * from './SlotData.js'; +export * from './SlotPose.js'; export * from './Texture.js'; export * from './TextureAtlas.js'; export * from './TransformConstraint.js'; export * from './TransformConstraintData.js'; +export * from './TransformConstraintPose.js'; export * from './Triangulator.js'; -export * from './Updatable.js'; +export * from './Update.js'; export * from './Utils.js'; export * from './polyfills.js'; export * from './attachments/Attachment.js'; export * from './attachments/AttachmentLoader.js'; export * from './attachments/BoundingBoxAttachment.js'; export * from './attachments/ClippingAttachment.js'; +export * from './attachments/HasTextureRegion.js'; export * from './attachments/MeshAttachment.js'; export * from './attachments/PathAttachment.js'; export * from './attachments/PointAttachment.js'; export * from './attachments/RegionAttachment.js'; +export * from './attachments/Sequence.js'; diff --git a/spine-ts/spine-phaser-v3/example/mix-and-match-example.html b/spine-ts/spine-phaser-v3/example/mix-and-match-example.html index 07571ba77..89fad51ef 100644 --- a/spine-ts/spine-phaser-v3/example/mix-and-match-example.html +++ b/spine-ts/spine-phaser-v3/example/mix-and-match-example.html @@ -70,7 +70,7 @@ skin.addSkin(skeletonData.findSkin("accessories/bag")); skin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow")); mixAndMatch.skeleton.setSkin(skin); - mixAndMatch.skeleton.setToSetupPose(); + mixAndMatch.skeleton.setupPose(); } diff --git a/spine-ts/spine-phaser-v3/src/SpineGameObject.ts b/spine-ts/spine-phaser-v3/src/SpineGameObject.ts index f5fa9a204..a7dfe15e0 100644 --- a/spine-ts/spine-phaser-v3/src/SpineGameObject.ts +++ b/spine-ts/spine-phaser-v3/src/SpineGameObject.ts @@ -96,7 +96,7 @@ export class SetupPoseBoundsProvider implements SpineGameObjectBoundsProvider { // the skeleton in the GameObject has already been heavily modified. We can not // reconstruct that state. const skeleton = new Skeleton(gameObject.skeleton.data); - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined); return bounds.width == Number.NEGATIVE_INFINITY @@ -145,7 +145,7 @@ export class SkinsAndAnimationBoundsProvider } skeleton.setSkin(customSkin); } - skeleton.setToSetupPose(); + skeleton.setupPose(); const animation = this.animation != null ? data.findAnimation(this.animation!) : null; @@ -161,7 +161,7 @@ export class SkinsAndAnimationBoundsProvider maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; animationState.clearTracks(); - animationState.setAnimationWith(0, animation, false); + animationState.setAnimation(0, animation, false); const steps = Math.max(animation.duration / this.timeStep, 1.0); for (let i = 0; i < steps; i++) { const delta = i > 0 ? this.timeStep : 0; @@ -296,9 +296,9 @@ export class SpineGameObject extends DepthMixin( phaserWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) { this.phaserWorldCoordinatesToSkeleton(point); if (bone.parent) { - bone.parent.worldToLocal(point as Vector2); + bone.parent.applied.worldToLocal(point as Vector2); } else { - bone.worldToLocal(point as Vector2); + bone.applied.worldToLocal(point as Vector2); } } @@ -418,7 +418,7 @@ export class SpineGameObject extends DepthMixin( skeleton.scaleX = transform.scaleX; skeleton.scaleY = transform.scaleY; let root = skeleton.getRootBone()!; - root.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized; + root.applied.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized; this.skeleton.updateWorldTransform(Physics.update); context.save(); diff --git a/spine-ts/spine-phaser-v4/example/mix-and-match-example.html b/spine-ts/spine-phaser-v4/example/mix-and-match-example.html index 36811df6c..54d508c85 100644 --- a/spine-ts/spine-phaser-v4/example/mix-and-match-example.html +++ b/spine-ts/spine-phaser-v4/example/mix-and-match-example.html @@ -70,7 +70,7 @@ skin.addSkin(skeletonData.findSkin("accessories/bag")); skin.addSkin(skeletonData.findSkin("accessories/hat-red-yellow")); mixAndMatch.skeleton.setSkin(skin); - mixAndMatch.skeleton.setToSetupPose(); + mixAndMatch.skeleton.setupPose(); } diff --git a/spine-ts/spine-phaser-v4/src/SpineGameObject.ts b/spine-ts/spine-phaser-v4/src/SpineGameObject.ts index cb99e6817..3c89abcf1 100644 --- a/spine-ts/spine-phaser-v4/src/SpineGameObject.ts +++ b/spine-ts/spine-phaser-v4/src/SpineGameObject.ts @@ -96,7 +96,7 @@ export class SetupPoseBoundsProvider implements SpineGameObjectBoundsProvider { // the skeleton in the GameObject has already been heavily modified. We can not // reconstruct that state. const skeleton = new Skeleton(gameObject.skeleton.data); - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined); return bounds.width == Number.NEGATIVE_INFINITY @@ -145,7 +145,7 @@ export class SkinsAndAnimationBoundsProvider } skeleton.setSkin(customSkin); } - skeleton.setToSetupPose(); + skeleton.setupPose(); const animation = this.animation != null ? data.findAnimation(this.animation!) : null; @@ -161,7 +161,7 @@ export class SkinsAndAnimationBoundsProvider maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; animationState.clearTracks(); - animationState.setAnimationWith(0, animation, false); + animationState.setAnimation(0, animation, false); const steps = Math.max(animation.duration / this.timeStep, 1.0); for (let i = 0; i < steps; i++) { const delta = i > 0 ? this.timeStep : 0; @@ -296,9 +296,9 @@ export class SpineGameObject extends DepthMixin( phaserWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) { this.phaserWorldCoordinatesToSkeleton(point); if (bone.parent) { - bone.parent.worldToLocal(point as Vector2); + bone.parent.applied.worldToLocal(point as Vector2); } else { - bone.worldToLocal(point as Vector2); + bone.applied.worldToLocal(point as Vector2); } } @@ -440,7 +440,7 @@ export class SpineGameObject extends DepthMixin( skeleton.scaleX = transform.scaleX; skeleton.scaleY = transform.scaleY; let root = skeleton.getRootBone()!; - root.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized; + root.applied.rotation = -MathUtils.radiansToDegrees * transform.rotationNormalized; this.skeleton.updateWorldTransform(Physics.update); context.save(); diff --git a/spine-ts/spine-pixi-v7/example/slot-objects.html b/spine-ts/spine-pixi-v7/example/slot-objects.html index d34bc74ee..8ac589671 100644 --- a/spine-ts/spine-pixi-v7/example/slot-objects.html +++ b/spine-ts/spine-pixi-v7/example/slot-objects.html @@ -119,8 +119,8 @@ // resetting the slot with the original attachment setTimeout(() => { - frontFist.setToSetupPose(); - frontFist.bone.setToSetupPose(); + frontFist.setupPose(); + frontFist.bone.setupPose(); }, 10000); // showing an animation with clipping -> Pixi masks will be created diff --git a/spine-ts/spine-pixi-v7/src/Spine.ts b/spine-ts/spine-pixi-v7/src/Spine.ts index 6f3d3ba76..5b5672746 100644 --- a/spine-ts/spine-pixi-v7/src/Spine.ts +++ b/spine-ts/spine-pixi-v7/src/Spine.ts @@ -168,7 +168,7 @@ export class SetupPoseBoundsProvider implements SpineBoundsProvider { // the skeleton in the GameObject has already been heavily modified. We can not // reconstruct that state. const skeleton = new Skeleton(gameObject.skeleton.data); - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined); return bounds.width == Number.NEGATIVE_INFINITY @@ -217,7 +217,7 @@ export class SkinsAndAnimationBoundsProvider } skeleton.setSkin(customSkin); } - skeleton.setToSetupPose(); + skeleton.setupPose(); const animation = this.animation != null ? data.findAnimation(this.animation!) : null; @@ -233,7 +233,7 @@ export class SkinsAndAnimationBoundsProvider maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; animationState.clearTracks(); - animationState.setAnimationWith(0, animation, false); + animationState.setAnimation(0, animation, false); const steps = Math.max(animation.duration / this.timeStep, 1.0); for (let i = 0; i < steps; i++) { const delta = i > 0 ? this.timeStep : 0; @@ -367,7 +367,7 @@ export class Spine extends Container { // dark tint can be enabled by options, otherwise is enable if at least one slot has tint black if (options?.darkTint !== undefined || oldOptions?.slotMeshFactory === undefined) { this.darkTint = options?.darkTint === undefined - ? this.skeleton.slots.some(slot => !!slot.data.darkColor) + ? this.skeleton.slots.some(slot => !!slot.data.setup.darkColor) : options?.darkTint; if (this.darkTint) this.slotMeshFactory = () => new DarkSlotMesh(); } else { @@ -390,7 +390,7 @@ export class Spine extends Container { tempSlotMeshFactory.destroy(); } else { for (let i = 0; i < this.skeleton.slots.length; i++) { - if (this.skeleton.slots[i].data.darkColor) { + if (this.skeleton.slots[i].data.setup.darkColor) { this.slotMeshFactory = () => new DarkSlotMesh(); this.darkTint = true; break; @@ -587,31 +587,33 @@ export class Spine extends Container { private updateSlotObject (element: { container: Container, followAttachmentTimeline: boolean }, slot: Slot, zIndex: number) { const { container: slotObject, followAttachmentTimeline } = element - const followAttachmentValue = followAttachmentTimeline ? Boolean(slot.attachment) : true; + const pose = slot.pose; + const followAttachmentValue = followAttachmentTimeline ? Boolean(pose.attachment) : true; slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue; if (slotObject.visible) { - slotObject.position.set(slot.bone.worldX, slot.bone.worldY); - slotObject.angle = slot.bone.getWorldRotationX(); + const applied = slot.bone.applied; + slotObject.position.set(applied.worldX, applied.worldY); + slotObject.angle = applied.getWorldRotationX(); let bone: Bone | null = slot.bone; let cumulativeScaleX = 1; let cumulativeScaleY = 1; while (bone) { - cumulativeScaleX *= bone.scaleX; - cumulativeScaleY *= bone.scaleY; + cumulativeScaleX *= bone.applied.scaleX; + cumulativeScaleY *= bone.applied.scaleY; bone = bone.parent; }; if (cumulativeScaleX < 0) slotObject.angle -= 180; slotObject.scale.set( - slot.bone.getWorldScaleX() * Math.sign(cumulativeScaleX), - slot.bone.getWorldScaleY() * Math.sign(cumulativeScaleY), + applied.getWorldScaleX() * Math.sign(cumulativeScaleX), + applied.getWorldScaleY() * Math.sign(cumulativeScaleY), ); slotObject.zIndex = zIndex + 1; - slotObject.alpha = this.skeleton.color.a * slot.color.a; + slotObject.alpha = this.skeleton.color.a * pose.color.a; } } @@ -625,10 +627,10 @@ export class Spine extends Container { } if (!pixiMaskSource.computed) { pixiMaskSource.computed = true; - const clippingAttachment = pixiMaskSource.slot.attachment as ClippingAttachment; + const clippingAttachment = pixiMaskSource.slot.pose.attachment as ClippingAttachment; const worldVerticesLength = clippingAttachment.worldVerticesLength; if (this.clippingVertAux.length < worldVerticesLength) this.clippingVertAux = new Float32Array(worldVerticesLength); - clippingAttachment.computeWorldVertices(pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2); + clippingAttachment.computeWorldVertices(this.skeleton, pixiMaskSource.slot, 0, worldVerticesLength, this.clippingVertAux, 0, 2); mask.clear().lineStyle(0).beginFill(0x000000); mask.moveTo(this.clippingVertAux[0], this.clippingVertAux[1]); for (let i = 2; i < worldVerticesLength; i += 2) { @@ -673,17 +675,19 @@ export class Spine extends Container { this.updateAndSetPixiMask(pixiMaskSource, pixiObject.container); } - const useDarkColor = slot.darkColor != null; + const pose = slot.pose; + const useDarkColor = !!pose.darkColor; const vertexSize = useDarkColor ? Spine.DARK_VERTEX_SIZE : Spine.VERTEX_SIZE; if (!slot.bone.active) { - Spine.clipper.clipEndWithSlot(slot); + Spine.clipper.clipEnd(slot); this.pixiMaskCleanup(slot); continue; } - const attachment = slot.getAttachment(); + const attachment = pose.attachment; let attachmentColor: Color | null; let texture: SpineTexture | null; let numFloats = 0; + const skeleton = this.skeleton; if (attachment instanceof RegionAttachment) { const region = attachment; attachmentColor = region.color; @@ -699,26 +703,25 @@ export class Spine extends Container { if (numFloats > this.verticesCache.length) { this.verticesCache = Utils.newFloatArray(numFloats); } - mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize); + mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize); triangles = mesh.triangles; uvs = mesh.uvs; texture = mesh.region?.texture; } else if (attachment instanceof ClippingAttachment) { - Spine.clipper.clipStart(slot, attachment); + Spine.clipper.clipStart(skeleton, slot, attachment); pixiMaskSource = { slot, computed: false }; continue; } else { if (this.hasMeshForSlot(slot)) { this.getMeshForSlot(slot).visible = false; } - Spine.clipper.clipEndWithSlot(slot); + Spine.clipper.clipEnd(slot); this.pixiMaskCleanup(slot); continue; } if (texture != null) { - const skeleton = slot.bone.skeleton; const skeletonColor = skeleton.color; - const slotColor = slot.color; + const slotColor = pose.color; const alpha = skeletonColor.a * slotColor.a * attachmentColor.a; // cannot premultiply the colors because the default mesh renderer already does that this.lightColor.set( @@ -727,11 +730,11 @@ export class Spine extends Container { skeletonColor.b * slotColor.b * attachmentColor.b, alpha ); - if (slot.darkColor != null) { + if (pose.darkColor != null) { this.darkColor.set( - slot.darkColor.r, - slot.darkColor.g, - slot.darkColor.b, + pose.darkColor.r, + pose.darkColor.g, + pose.darkColor.b, 1, ); } else { @@ -775,7 +778,7 @@ export class Spine extends Container { } if (finalVerticesLength == 0 || finalIndicesLength == 0) { - Spine.clipper.clipEndWithSlot(slot); + Spine.clipper.clipEnd(slot); continue; } @@ -785,7 +788,7 @@ export class Spine extends Container { mesh.updateFromSpineData(texture, slot.data.blendMode, slot.data.name, finalVertices, finalVerticesLength, finalIndices, finalIndicesLength, useDarkColor); } - Spine.clipper.clipEndWithSlot(slot); + Spine.clipper.clipEnd(slot); this.pixiMaskCleanup(slot); } Spine.clipper.clipEnd(); @@ -852,14 +855,14 @@ export class Spine extends Container { if (!bone) throw Error(`Cannot set bone position, bone ${String(boneAux)} not found`); Spine.vectorAux.set(position.x, position.y); + const applied = bone.applied; if (bone.parent) { - const aux = bone.parent.worldToLocal(Spine.vectorAux); - bone.x = aux.x; - bone.y = aux.y; - } - else { - bone.x = Spine.vectorAux.x; - bone.y = Spine.vectorAux.y; + const aux = bone.parent.applied.worldToLocal(Spine.vectorAux); + applied.x = aux.x; + applied.y = aux.y; + } else { + applied.x = Spine.vectorAux.x; + applied.y = Spine.vectorAux.y; } } @@ -884,8 +887,8 @@ export class Spine extends Container { outPos = { x: 0, y: 0 }; } - outPos.x = bone.worldX; - outPos.y = bone.worldY; + outPos.x = bone.applied.worldX; + outPos.y = bone.applied.worldY; return outPos; } @@ -903,9 +906,9 @@ export class Spine extends Container { pixiWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) { this.pixiWorldCoordinatesToSkeleton(point); if (bone.parent) { - bone.parent.worldToLocal(point as Vector2); + bone.parent.applied.worldToLocal(point as Vector2); } else { - bone.worldToLocal(point as Vector2); + bone.applied.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 c606fc4e2..3bba21989 100644 --- a/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts +++ b/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts @@ -243,10 +243,11 @@ 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 starX = skeletonX + bone.worldX; - const starY = skeletonY + bone.worldY; - const endX = skeletonX + boneLen * bone.a + bone.worldX; - const endY = skeletonY + boneLen * bone.b + bone.worldY; + const applied = bone.applied; + const starX = skeletonX + applied.worldX; + const starY = skeletonY + applied.worldY; + const endX = skeletonX + boneLen * applied.a + applied.worldX; + const endY = skeletonY + boneLen * applied.b + applied.worldY; if (bone.data.name === "root" || bone.data.parent === null) { continue; @@ -337,7 +338,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { for (let i = 0, len = slots.length; i < len; i++) { const slot = slots[i]; - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment == null || !(attachment instanceof RegionAttachment)) { continue; @@ -365,7 +366,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { if (!slot.bone.active) { continue; } - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment == null || !(attachment instanceof MeshAttachment)) { continue; @@ -377,7 +378,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { const triangles = meshAttachment.triangles; let hullLength = meshAttachment.hullLength; - meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); + meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); // draw the skinned mesh (triangle) if (this.drawMeshTriangles) { for (let i = 0, len = triangles.length; i < len; i += 3) { @@ -421,7 +422,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { if (!slot.bone.active) { continue; } - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment == null || !(attachment instanceof ClippingAttachment)) { continue; @@ -432,7 +433,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { const nn = clippingAttachment.worldVerticesLength; const world = new Float32Array(nn); - clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + clippingAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); debugDisplayObjects.clippingPolygon.drawPolygon(Array.from(world)); } } @@ -496,7 +497,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { if (!slot.bone.active) { continue; } - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment == null || !(attachment instanceof PathAttachment)) { continue; @@ -506,7 +507,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { let nn = pathAttachment.worldVerticesLength; const world = new Float32Array(nn); - pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + pathAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); let x1 = world[2]; let y1 = world[3]; let x2 = 0; diff --git a/spine-ts/spine-pixi-v8/example/slot-objects.html b/spine-ts/spine-pixi-v8/example/slot-objects.html index a1f069994..11f310d64 100644 --- a/spine-ts/spine-pixi-v8/example/slot-objects.html +++ b/spine-ts/spine-pixi-v8/example/slot-objects.html @@ -118,8 +118,8 @@ // resetting the slot with the original attachment setTimeout(() => { - frontFist.setToSetupPose(); - frontFist.bone.setToSetupPose(); + frontFist.setupPose(); + frontFist.bone.setupPose(); }, 10000); // showing an animation with clipping -> Pixi masks will be created diff --git a/spine-ts/spine-pixi-v8/src/Spine.ts b/spine-ts/spine-pixi-v8/src/Spine.ts index a180a2507..6a009f49e 100644 --- a/spine-ts/spine-pixi-v8/src/Spine.ts +++ b/spine-ts/spine-pixi-v8/src/Spine.ts @@ -139,7 +139,7 @@ export class SetupPoseBoundsProvider implements SpineBoundsProvider { // the skeleton in the GameObject has already been heavily modified. We can not // reconstruct that state. const skeleton = new Skeleton(gameObject.skeleton.data); - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(Physics.update); const bounds = skeleton.getBoundsRect(this.clipping ? new SkeletonClipping() : undefined); return bounds.width == Number.NEGATIVE_INFINITY @@ -188,7 +188,7 @@ export class SkinsAndAnimationBoundsProvider } skeleton.setSkin(customSkin); } - skeleton.setToSetupPose(); + skeleton.setupPose(); const animation = this.animation != null ? data.findAnimation(this.animation!) : null; @@ -204,7 +204,7 @@ export class SkinsAndAnimationBoundsProvider maxX = Number.NEGATIVE_INFINITY, maxY = Number.NEGATIVE_INFINITY; animationState.clearTracks(); - animationState.setAnimationWith(0, animation, false); + animationState.setAnimation(0, animation, false); const steps = Math.max(animation.duration / this.timeStep, 1.0); for (let i = 0; i < steps; i++) { const delta = i > 0 ? this.timeStep : 0; @@ -400,7 +400,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 = options?.darkTint === undefined - ? this.skeleton.slots.some(slot => !!slot.data.darkColor) + ? this.skeleton.slots.some(slot => !!slot.data.setup.darkColor) : options?.darkTint; const slots = this.skeleton.slots; @@ -447,15 +447,16 @@ 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; if (bone.parent) { - const aux = bone.parent.worldToLocal(vectorAux); + const aux = bone.parent.applied.worldToLocal(vectorAux); - bone.x = aux.x; - bone.y = -aux.y; + applied.x = aux.x; + applied.y = -aux.y; } else { - bone.x = vectorAux.x; - bone.y = vectorAux.y; + applied.x = vectorAux.x; + applied.y = vectorAux.y; } } @@ -482,8 +483,8 @@ export class Spine extends ViewContainer { outPos = { x: 0, y: 0 }; } - outPos.x = bone.worldX; - outPos.y = bone.worldY; + outPos.x = bone.applied.worldX; + outPos.y = bone.applied.worldY; return outPos; } @@ -541,7 +542,7 @@ export class Spine extends ViewContainer { for (let i = 0; i < currentDrawOrder.length; i++) { const slot = currentDrawOrder[i]; - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment) { if (attachment !== lastAttachments[index]) { @@ -564,7 +565,8 @@ export class Spine extends ViewContainer { private currentClippingSlot: SlotsToClipping | undefined; private updateAndSetPixiMask (slot: Slot, last: boolean) { // assign/create the currentClippingSlot - const attachment = slot.attachment; + const pose = slot.pose; + const attachment = pose.attachment; if (attachment && attachment instanceof ClippingAttachment) { const clip = (this.clippingSlotToPixiMasks[slot.data.name] ||= { slot, vertices: new Array() }); clip.maskComputed = false; @@ -577,7 +579,7 @@ export class Spine extends ViewContainer { let slotObject = this._slotsObject[slot.data.name]; if (currentClippingSlot && slotObject) { let slotClipping = currentClippingSlot.slot; - let clippingAttachment = slotClipping.attachment as ClippingAttachment; + let clippingAttachment = slotClipping.pose.attachment as ClippingAttachment; // create the pixi mask, only the first time and if the clipped slot is the first one clipped by this currentClippingSlot let mask = currentClippingSlot.mask as Graphics; @@ -592,7 +594,7 @@ export class Spine extends ViewContainer { currentClippingSlot.maskComputed = true; const worldVerticesLength = clippingAttachment.worldVerticesLength; const vertices = currentClippingSlot.vertices; - clippingAttachment.computeWorldVertices(slotClipping, 0, worldVerticesLength, vertices, 0, 2); + clippingAttachment.computeWorldVertices(this.skeleton, slotClipping, 0, worldVerticesLength, vertices, 0, 2); mask.clear().poly(vertices).stroke({ width: 0 }).fill({ alpha: .25 }); } slotObject.container.mask = mask; @@ -602,7 +604,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.attachment as ClippingAttachment).endSlot == slot.data) { + if (currentClippingSlot && (currentClippingSlot.slot.pose.attachment as ClippingAttachment).endSlot == slot.data) { this.currentClippingSlot = undefined; } @@ -610,7 +612,7 @@ export class Spine extends ViewContainer { if (last) { for (const key in this.clippingSlotToPixiMasks) { const clippingSlotToPixiMask = this.clippingSlotToPixiMasks[key]; - if ((!(clippingSlotToPixiMask.slot.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) { + if ((!(clippingSlotToPixiMask.slot.pose.attachment instanceof ClippingAttachment) || !clippingSlotToPixiMask.maskComputed) && clippingSlotToPixiMask.mask) { this.removeChild(clippingSlotToPixiMask.mask); maskPool.free(clippingSlotToPixiMask.mask); clippingSlotToPixiMask.mask = undefined; @@ -622,13 +624,15 @@ export class Spine extends ViewContainer { private transformAttachments () { const currentDrawOrder = this.skeleton.drawOrder; + const skeleton = this.skeleton; for (let i = 0; i < currentDrawOrder.length; i++) { const slot = currentDrawOrder[i]; this.updateAndSetPixiMask(slot, i === currentDrawOrder.length - 1); - const attachment = slot.getAttachment(); + const pose = slot.pose; + const attachment = pose; if (attachment) { if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) { @@ -639,6 +643,7 @@ export class Spine extends ViewContainer { } else { attachment.computeWorldVertices( + skeleton, slot, 0, attachment.worldVerticesLength, @@ -656,9 +661,8 @@ export class Spine extends ViewContainer { // need to copy because attachments uvs are shared among skeletons using the same atlas fastCopy((attachment.uvs as Float32Array).buffer, cacheData.uvs.buffer); - const skeleton = slot.bone.skeleton; const skeletonColor = skeleton.color; - const slotColor = slot.color; + const slotColor = pose.color; const attachmentColor = attachment.color; @@ -669,8 +673,8 @@ export class Spine extends ViewContainer { skeletonColor.a * slotColor.a * attachmentColor.a, ); - if (slot.darkColor) { - cacheData.darkColor.setFromColor(slot.darkColor); + if (pose.darkColor) { + cacheData.darkColor.setFromColor(pose.darkColor); } cacheData.skipRender = cacheData.clipped = false; @@ -687,11 +691,11 @@ export class Spine extends ViewContainer { } } else if (attachment instanceof ClippingAttachment) { - clipper.clipStart(slot, attachment); + clipper.clipStart(skeleton, slot, attachment); continue; } } - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); } clipper.clipEnd(); } @@ -782,31 +786,33 @@ export class Spine extends ViewContainer { private updateSlotObject (slotAttachment: { slot: Slot, container: Container, followAttachmentTimeline: boolean }) { const { slot, container } = slotAttachment; - const followAttachmentValue = slotAttachment.followAttachmentTimeline ? Boolean(slot.attachment) : true; + const pose = slot.pose; + const followAttachmentValue = slotAttachment.followAttachmentTimeline ? Boolean(pose.attachment) : true; container.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue; if (container.visible) { let bone: Bone | null = slot.bone; - container.position.set(bone.worldX, bone.worldY); - container.angle = bone.getWorldRotationX(); + const applied = bone.applied; + container.position.set(applied.worldX, applied.worldY); + container.angle = applied.getWorldRotationX(); let cumulativeScaleX = 1; let cumulativeScaleY = 1; while (bone) { - cumulativeScaleX *= bone.scaleX; - cumulativeScaleY *= bone.scaleY; + cumulativeScaleX *= bone.applied.scaleX; + cumulativeScaleY *= bone.applied.scaleY; bone = bone.parent; }; if (cumulativeScaleX < 0) container.angle -= 180; container.scale.set( - slot.bone.getWorldScaleX() * Math.sign(cumulativeScaleX), - slot.bone.getWorldScaleY() * Math.sign(cumulativeScaleY), + applied.getWorldScaleX() * Math.sign(cumulativeScaleX), + applied.getWorldScaleY() * Math.sign(cumulativeScaleY), ); - container.alpha = this.skeleton.color.a * slot.color.a; + container.alpha = this.skeleton.color.a * pose.color.a; } } @@ -999,7 +1005,7 @@ export class Spine extends ViewContainer { for (let i = 0; i < drawOrder.length; i++) { const slot = drawOrder[i]; - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment && (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) { const cacheData = this._getCachedData(slot, attachment); @@ -1055,10 +1061,10 @@ export class Spine extends ViewContainer { public pixiWorldCoordinatesToBone (point: { x: number; y: number }, bone: Bone) { this.pixiWorldCoordinatesToSkeleton(point); if (bone.parent) { - bone.parent.worldToLocal(point as Vector2); + bone.parent.applied.worldToLocal(point as Vector2); } else { - bone.worldToLocal(point as Vector2); + bone.applied.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 7d6360992..d8e2c1717 100644 --- a/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts +++ b/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts @@ -259,10 +259,11 @@ 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 starX = skeletonX + bone.worldX; - const starY = skeletonY + bone.worldY; - const endX = skeletonX + (boneLen * bone.a) + bone.worldX; - const endY = skeletonY + (boneLen * bone.b) + bone.worldY; + const applied = bone.applied; + const starX = skeletonX + applied.worldX; + const starY = skeletonY + applied.worldY; + const endX = skeletonX + (boneLen * applied.a) + applied.worldX; + const endY = skeletonY + (boneLen * applied.b) + applied.worldY; if (bone.data.name === 'root' || bone.data.parent === null) { continue; @@ -359,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.getAttachment(); + const attachment = slot.pose.attachment; if (attachment === null || !(attachment instanceof RegionAttachment)) { continue; @@ -390,7 +391,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { if (!slot.bone.active) { continue; } - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment === null || !(attachment instanceof MeshAttachment)) { continue; @@ -402,7 +403,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { const triangles = meshAttachment.triangles; let hullLength = meshAttachment.hullLength; - meshAttachment.computeWorldVertices(slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); + meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, vertices, 0, 2); // draw the skinned mesh (triangle) if (this.drawMeshTriangles) { for (let i = 0, len = triangles.length; i < len; i += 3) { @@ -450,7 +451,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { if (!slot.bone.active) { continue; } - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment === null || !(attachment instanceof ClippingAttachment)) { continue; @@ -461,7 +462,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { const nn = clippingAttachment.worldVerticesLength; const world = new Float32Array(nn); - clippingAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + clippingAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); debugDisplayObjects.clippingPolygon.poly(Array.from(world)); } @@ -535,7 +536,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { if (!slot.bone.active) { continue; } - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment === null || !(attachment instanceof PathAttachment)) { continue; @@ -545,7 +546,7 @@ export class SpineDebugRenderer implements ISpineDebugRenderer { let nn = pathAttachment.worldVerticesLength; const world = new Float32Array(nn); - pathAttachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + pathAttachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); let x1 = world[2]; let y1 = world[3]; let x2 = 0; diff --git a/spine-ts/spine-pixi-v8/src/SpinePipe.ts b/spine-ts/spine-pixi-v8/src/SpinePipe.ts index 4d0465cae..67538cd7c 100644 --- a/spine-ts/spine-pixi-v8/src/SpinePipe.ts +++ b/spine-ts/spine-pixi-v8/src/SpinePipe.ts @@ -86,7 +86,7 @@ export class SpinePipe implements RenderPipe { for (let i = 0, n = drawOrder.length; i < n; i++) { const slot = drawOrder[i]; - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) { const cacheData = spine._getCachedData(slot, attachment); @@ -122,7 +122,7 @@ export class SpinePipe implements RenderPipe { for (let i = 0, n = drawOrder.length; i < n; i++) { const slot = drawOrder[i]; - const attachment = slot.getAttachment(); + const attachment = slot.pose.attachment; const blendMode = spineBlendModeMap[slot.data.blendMode]; if (attachment instanceof RegionAttachment || attachment instanceof MeshAttachment) { @@ -165,7 +165,7 @@ export class SpinePipe implements RenderPipe { for (let i = 0, n = drawOrder.length; i < n; i++) { const slot = drawOrder[i]; - const attachment = slot.getAttachment(); + const attachment = slot.pose.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 43cd469b0..3aa849e80 100644 --- a/spine-ts/spine-player/src/Player.ts +++ b/spine-ts/spine-player/src/Player.ts @@ -528,8 +528,8 @@ export class SpinePlayer implements Disposable { if (config.skin) { if (!this.skeleton.data.findSkin(config.skin)) this.showError(`Error: Skin does not exist in skeleton: ${config.skin}`); - this.skeleton.setSkinByName(config.skin); - this.skeleton.setSlotsToSetupPose(); + this.skeleton.setSkin(config.skin); + this.skeleton.setupPoseSlots(); } // Check if all animations given a viewport exist. @@ -609,7 +609,7 @@ export class SpinePlayer implements Disposable { let bone = skeleton.findBone(controlBones[i]); if (!bone) continue; let distance = renderer.camera.worldToScreen( - coords.set(bone.worldX, bone.worldY, 0), + coords.set(bone.applied.worldX, bone.applied.worldY, 0), canvas.clientWidth, canvas.clientHeight).distance(mouse); if (distance < bestDistance) { bestDistance = distance; @@ -639,13 +639,14 @@ 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; if (target.parent) { - target.parent.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y)); - target.x = position.x; - target.y = position.y; + target.parent.applied.worldToLocal(position.set(coords.x - skeleton.x, coords.y - skeleton.y)); + applied.x = position.x; + applied.y = position.y; } else { - target.x = coords.x - skeleton.x; - target.y = coords.y - skeleton.y; + applied.x = coords.x - skeleton.x; + applied.y = coords.y - skeleton.y; } } }, @@ -730,13 +731,13 @@ export class SpinePlayer implements Disposable { /* Sets a new animation and viewport on track 0. */ setAnimation (animation: string | Animation, loop: boolean = true): TrackEntry { animation = this.setViewport(animation); - return this.animationState!.setAnimationWith(0, animation, loop); + return this.animationState!.setAnimation(0, animation, loop); } /* Adds a new animation and viewport on track 0. */ addAnimation (animation: string | Animation, loop: boolean = true, delay: number = 0): TrackEntry { animation = this.setViewport(animation); - return this.animationState!.addAnimationWith(0, animation, loop, delay); + return this.animationState!.addAnimation(0, animation, loop, delay); } /* Sets the viewport for the specified animation. */ @@ -799,7 +800,7 @@ export class SpinePlayer implements Disposable { } private calculateAnimationViewport (animation: Animation, viewport: Viewport) { - this.skeleton!.setToSetupPose(); + this.skeleton!.setupPose(); let steps = 100, stepTime = animation.duration ? animation.duration / steps : 0, time = 0; let minX = 100000000, maxX = -100000000, minY = 100000000, maxY = -100000000; @@ -807,7 +808,7 @@ export class SpinePlayer implements Disposable { const tempArray = new Array(2); for (let i = 0; i < steps; i++, time += stepTime) { - animation.apply(this.skeleton!, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn); + animation.apply(this.skeleton!, time, time, false, [], 1, MixBlend.setup, MixDirection.in, false); this.skeleton!.updateWorldTransform(Physics.update); this.skeleton!.getBounds(offset, size, tempArray, this.sceneRenderer!.skeletonRenderer.getSkeletonClipping()); @@ -943,8 +944,9 @@ export class SpinePlayer implements Disposable { if (!bone) continue; let colorInner = selectedBones[i] ? BONE_INNER_OVER : BONE_INNER; let colorOuter = selectedBones[i] ? BONE_OUTER_OVER : BONE_OUTER; - renderer.circle(true, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorInner); - renderer.circle(false, skeleton.x + bone.worldX, skeleton.y + bone.worldY, 20, colorOuter); + const applied = bone.applied; + 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); } } @@ -1055,8 +1057,8 @@ export class SpinePlayer implements Disposable { removeClass(rows.children, "selected"); row.classList.add("selected"); this.config.skin = skin.name; - this.skeleton!.setSkinByName(this.config.skin); - this.skeleton!.setSlotsToSetupPose(); + this.skeleton!.setSkin(this.config.skin); + this.skeleton!.setupPose(); } }); popup.show(); diff --git a/spine-ts/spine-threejs/src/SkeletonMesh.ts b/spine-ts/spine-threejs/src/SkeletonMesh.ts index 357a2621b..2bd9796bb 100644 --- a/spine-ts/spine-threejs/src/SkeletonMesh.ts +++ b/spine-ts/spine-threejs/src/SkeletonMesh.ts @@ -219,14 +219,13 @@ export class SkeletonMesh extends THREE.Object3D { private updateGeometry () { this.clearBatches(); - let tempLight = this.tempLight; - let tempDark = this.tempDark; let clipper = this.clipper; let vertices: NumberArrayLike = this.vertices; let triangles: Array | null = null; let uvs: NumberArrayLike | null = null; - let drawOrder = this.skeleton.drawOrder; + const skeleton = this.skeleton; + let drawOrder = skeleton.drawOrder; let batch = this.nextBatch(); batch.begin(); let z = 0; @@ -235,10 +234,11 @@ export class SkeletonMesh extends THREE.Object3D { for (let i = 0, n = drawOrder.length; i < n; i++) { let slot = drawOrder[i]; if (!slot.bone.active) { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } - let attachment = slot.getAttachment(); + let pose = slot.pose; + let attachment = pose.attachment; let attachmentColor: Color | null; let texture: ThreeJsTexture | null; let numFloats = 0; @@ -258,6 +258,7 @@ export class SkeletonMesh extends THREE.Object3D { vertices = this.vertices = Utils.newFloatArray(numFloats); } attachment.computeWorldVertices( + skeleton, slot, 0, attachment.worldVerticesLength, @@ -269,17 +270,17 @@ export class SkeletonMesh extends THREE.Object3D { uvs = attachment.uvs; texture = attachment.region!.texture; } else if (attachment instanceof ClippingAttachment) { - clipper.clipStart(slot, attachment); + clipper.clipEnd(slot); + clipper.clipStart(skeleton, slot, attachment); continue; } else { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } if (texture != null) { - let skeleton = slot.bone.skeleton; let skeletonColor = skeleton.color; - let slotColor = slot.color; + let slotColor = pose.color; let alpha = skeletonColor.a * slotColor.a * attachmentColor.a; let color = this.tempColor; color.set( @@ -290,12 +291,12 @@ export class SkeletonMesh extends THREE.Object3D { ); let darkColor = this.tempDarkColor; - if (!slot.darkColor) + if (!pose.darkColor) darkColor.set(0, 0, 0, 1); else { - darkColor.r = slot.darkColor.r * alpha; - darkColor.g = slot.darkColor.g * alpha; - darkColor.b = slot.darkColor.b * alpha; + darkColor.r = pose.darkColor.r * alpha; + darkColor.g = pose.darkColor.g * alpha; + darkColor.b = pose.darkColor.b * alpha; darkColor.a = 1; } @@ -304,7 +305,7 @@ export class SkeletonMesh extends THREE.Object3D { let finalIndices: NumberArrayLike; let finalIndicesLength: number; - if (clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length, uvs, color, tempLight, this.twoColorTint, vertexSize)) { + if (clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length, uvs, color, darkColor, this.twoColorTint, vertexSize)) { let clippedVertices = clipper.clippedVertices; let clippedTriangles = clipper.clippedTriangles; finalVertices = clippedVertices; @@ -344,7 +345,7 @@ export class SkeletonMesh extends THREE.Object3D { } if (finalVerticesLength == 0 || finalIndicesLength == 0) { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } @@ -378,7 +379,7 @@ export class SkeletonMesh extends THREE.Object3D { z += zOffset; } - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); } clipper.clipEnd(); batch.end(); diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts index 07ca453ed..5ecc73bbf 100644 --- a/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts +++ b/spine-ts/spine-webcomponents/src/SpineWebComponentOverlay.ts @@ -642,7 +642,7 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr // show skeleton root const root = skeleton.getRootBone()!; - renderer.circle(true, root.x + worldOffsetX, root.y + worldOffsetY, 10, red); + renderer.circle(true, root.applied.x + worldOffsetX, root.applied.y + worldOffsetY, 10, red); // show shifted origin renderer.circle(true, divOriginX, divOriginY, 10, green); @@ -665,7 +665,8 @@ 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; - this.worldToScreen(this.tempFollowBoneVector, bone.worldX + worldX, bone.worldY + worldY); + const applied = bone.applied; + this.worldToScreen(this.tempFollowBoneVector, applied.worldX + worldX, applied.worldY + worldY); if (Number.isNaN(this.tempFollowBoneVector.x)) continue; @@ -678,16 +679,17 @@ export class SpineWebComponentOverlay extends HTMLElement implements OverlayAttr } element.style.transform = `translate(calc(-50% + ${x.toFixed(2)}px),calc(-50% + ${y.toFixed(2)}px))` - + (followRotation ? ` rotate(${-bone.getWorldRotationX()}deg)` : "") - + (followScale ? ` scale(${bone.getWorldScaleX()}, ${bone.getWorldScaleY()})` : "") + + (followRotation ? ` rotate(${-applied.getWorldRotationX()}deg)` : "") + + (followScale ? ` scale(${applied.getWorldScaleX()}, ${applied.getWorldScaleY()})` : "") ; element.style.display = "" - if (followVisibility && !slot.attachment) { + const pose = slot.pose; + if (followVisibility && !pose.attachment) { element.style.opacity = "0"; } else if (followOpacity) { - element.style.opacity = `${slot.color.a}`; + element.style.opacity = `${pose.color.a}`; } } diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts index 4ca7af168..ad09de4ea 100644 --- a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts +++ b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts @@ -1013,14 +1013,14 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable if (skin) { if (skin.length === 1) { - skeleton?.setSkinByName(skin[0]); + skeleton?.setSkin(skin[0]); } else { const customSkin = new Skin("custom"); for (const s of skin) customSkin.addSkin(skeleton?.data.findSkin(s) as Skin); skeleton?.setSkin(customSkin); } - skeleton?.setSlotsToSetupPose(); + skeleton?.setupPoseSlots(); } if (state) { @@ -1156,7 +1156,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable private checkSlotInteraction (type: PointerEventTypesInput, originalEvent?: UIEvent) { for (let [slot, interactionState] of this.pointerSlotEventCallbacks) { if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); + let attachment = slot.pose.attachment; if (!(attachment instanceof RegionAttachment || attachment instanceof MeshAttachment)) continue; @@ -1171,7 +1171,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable regionAttachment.computeWorldVertices(slot, vertices, 0, 2); } else if (attachment instanceof MeshAttachment) { let mesh = attachment; - mesh.computeWorldVertices(slot, 0, mesh.worldVerticesLength, vertices, 0, 2); + mesh.computeWorldVertices(this.skeleton!, slot, 0, mesh.worldVerticesLength, vertices, 0, 2); hullLength = mesh.hullLength; } @@ -1245,7 +1245,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable if (!slot) return; if (hideAttachment) { - slot.setAttachment(null); + slot.pose.setAttachment(null); } element.style.position = 'absolute'; @@ -1271,7 +1271,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable const renderer = this.overlay.renderer; const { skeleton } = this; if (!skeleton) return { x: 0, y: 0, width: 0, height: 0 }; - skeleton.setToSetupPose(); + skeleton.setupPose(); let offset = new Vector2(), size = new Vector2(); const tempArray = new Array(2); @@ -1289,7 +1289,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.mixIn); + animation.apply(skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.in, false); skeleton.updateWorldTransform(Physics.update); skeleton.getBounds(offset, size, tempArray, renderer.skeletonRenderer.getSkeletonClipping()); @@ -1304,7 +1304,7 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable } } - skeleton.setToSetupPose(); + skeleton.setupPose(); return { x: minX, diff --git a/spine-ts/spine-webgl/example/barebones.html b/spine-ts/spine-webgl/example/barebones.html index 29497c996..9d93af7fc 100644 --- a/spine-ts/spine-webgl/example/barebones.html +++ b/spine-ts/spine-webgl/example/barebones.html @@ -83,4 +83,4 @@ - \ No newline at end of file +` \ No newline at end of file diff --git a/spine-ts/spine-webgl/example/dress-up.html b/spine-ts/spine-webgl/example/dress-up.html index b2ee922ae..31c881cb0 100644 --- a/spine-ts/spine-webgl/example/dress-up.html +++ b/spine-ts/spine-webgl/example/dress-up.html @@ -115,7 +115,7 @@ newSkin.addSkin(this.skeletonData.findSkin(skinName)); } this.skeleton.setSkin(newSkin); - this.skeleton.setToSetupPose(); + this.skeleton.setupPose(); this.skeleton.updateWorldTransform(spine.Physics.update); // Calculate the bounds so we can center and zoom @@ -153,7 +153,7 @@ // Set the skin, then update the skeleton // to the setup pose and calculate the world transforms this.skeleton.setSkin(skin); - this.skeleton.setToSetupPose(); + this.skeleton.setupPose(); this.skeleton.updateWorldTransform(spine.Physics.update); // Calculate the bounding box enclosing the skeleton. diff --git a/spine-ts/spine-webgl/example/index.html b/spine-ts/spine-webgl/example/index.html index 4f0da13ff..5940df695 100644 --- a/spine-ts/spine-webgl/example/index.html +++ b/spine-ts/spine-webgl/example/index.html @@ -223,7 +223,7 @@ } function calculateSetupPoseBounds(skeleton) { - skeleton.setToSetupPose(); + skeleton.setupPose(); skeleton.updateWorldTransform(spine.Physics.update); let offset = new spine.Vector2(); let size = new spine.Vector2(); @@ -260,7 +260,7 @@ let state = skeletons[activeSkeleton][format].state; let skeleton = skeletons[activeSkeleton][format].skeleton; let animationName = $("#animationList option:selected").text(); - skeleton.setToSetupPose(); + skeleton.setupPose(); state.setAnimation(0, animationName, true); }) } diff --git a/spine-ts/spine-webgl/example/physics2.html b/spine-ts/spine-webgl/example/physics2.html index 4de82355d..38bb7692c 100644 --- a/spine-ts/spine-webgl/example/physics2.html +++ b/spine-ts/spine-webgl/example/physics2.html @@ -66,7 +66,7 @@ // Center the camera on the skeleton const offset = new spine.Vector2(); const size = new spine.Vector2(); - this.skeleton.setToSetupPose(); + this.skeleton.setupPose(); this.skeleton.update(0); this.skeleton.updateWorldTransform(spine.Physics.update); this.skeleton.getBounds(offset, size); diff --git a/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts b/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts index 001ab96ec..6b8478274 100644 --- a/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts +++ b/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts @@ -76,9 +76,10 @@ export class SkeletonDebugRenderer implements Disposable { let bone = bones[i]; if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue; if (!bone.parent) continue; - let x = bone.data.length * bone.a + bone.worldX; - let y = bone.data.length * bone.c + bone.worldY; - shapes.rectLine(true, bone.worldX, bone.worldY, x, y, this.boneWidth * this.scale); + const boneApplied = bone.applied; + let x = bone.data.length * boneApplied.a + boneApplied.worldX; + let y = bone.data.length * boneApplied.c + boneApplied.worldY; + shapes.rectLine(true, boneApplied.worldX, boneApplied.worldY, x, y, this.boneWidth * this.scale); } if (this.drawSkeletonXY) shapes.x(skeletonX, skeletonY, 4 * this.scale); } @@ -89,7 +90,7 @@ export class SkeletonDebugRenderer implements Disposable { for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); + let attachment = slot.pose.attachment; if (attachment instanceof RegionAttachment) { let vertices = this.vertices; attachment.computeWorldVertices(slot, vertices, 0, 2); @@ -106,10 +107,10 @@ export class SkeletonDebugRenderer implements Disposable { for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); + let attachment = slot.pose.attachment; if (!(attachment instanceof MeshAttachment)) continue; let vertices = this.vertices; - attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, vertices, 0, 2); + attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2); let triangles = attachment.triangles; let hullLength = attachment.hullLength; if (this.drawMeshTriangles) { @@ -155,11 +156,11 @@ export class SkeletonDebugRenderer implements Disposable { for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); + let attachment = slot.pose.attachment; if (!(attachment instanceof PathAttachment)) continue; let nn = attachment.worldVerticesLength; let world = this.temp = Utils.setArraySize(this.temp, nn, 0); - attachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); let color = this.pathColor; let x1 = world[2], y1 = world[3], x2 = 0, y2 = 0; if (attachment.closed) { @@ -193,7 +194,8 @@ export class SkeletonDebugRenderer implements Disposable { for (let i = 0, n = bones.length; i < n; i++) { let bone = bones[i]; if (ignoredBones && ignoredBones.indexOf(bone.data.name) > -1) continue; - shapes.circle(true, bone.worldX, bone.worldY, 3 * this.scale, this.boneOriginColor, 8); + let boneApplied = bone.applied; + shapes.circle(true, boneApplied.worldX, boneApplied.worldY, 3 * this.scale, this.boneOriginColor, 8); } } @@ -203,11 +205,11 @@ export class SkeletonDebugRenderer implements Disposable { for (let i = 0, n = slots.length; i < n; i++) { let slot = slots[i]; if (!slot.bone.active) continue; - let attachment = slot.getAttachment(); + let attachment = slot.pose.attachment; if (!(attachment instanceof ClippingAttachment)) continue; let nn = attachment.worldVerticesLength; let world = this.temp = Utils.setArraySize(this.temp, nn, 0); - attachment.computeWorldVertices(slot, 0, nn, world, 0, 2); + attachment.computeWorldVertices(skeleton, slot, 0, nn, world, 0, 2); for (let i = 0, n = world.length; i < n; i += 2) { let x = world[i]; let y = world[i + 1]; diff --git a/spine-ts/spine-webgl/src/SkeletonRenderer.ts b/spine-ts/spine-webgl/src/SkeletonRenderer.ts index 15c82c7e1..72622d144 100644 --- a/spine-ts/spine-webgl/src/SkeletonRenderer.ts +++ b/spine-ts/spine-webgl/src/SkeletonRenderer.ts @@ -80,7 +80,7 @@ export class SkeletonRenderer { for (let i = 0, n = drawOrder.length; i < n; i++) { let slot = drawOrder[i]; if (!slot.bone.active) { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } @@ -89,7 +89,7 @@ export class SkeletonRenderer { } if (!inRange) { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } @@ -97,7 +97,8 @@ export class SkeletonRenderer { inRange = false; } - let attachment = slot.getAttachment(); + const pose = slot.pose; + const attachment = pose.attachment; let texture: GLTexture; if (attachment instanceof RegionAttachment) { renderable.vertices = this.vertices; @@ -116,21 +117,22 @@ export class SkeletonRenderer { if (renderable.numFloats > renderable.vertices.length) { renderable.vertices = this.vertices = Utils.newFloatArray(renderable.numFloats); } - attachment.computeWorldVertices(slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize); + attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize); triangles = attachment.triangles; texture = attachment.region!.texture; uvs = attachment.uvs; attachmentColor = attachment.color; } else if (attachment instanceof ClippingAttachment) { - clipper.clipStart(slot, attachment); + clipper.clipEnd(slot); + clipper.clipStart(skeleton, slot, attachment); continue; } else { - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); continue; } if (texture) { - let slotColor = slot.color; + let slotColor = pose.color; let finalColor = this.tempColor; finalColor.r = skeletonColor.r * slotColor.r * attachmentColor.r; finalColor.g = skeletonColor.g * slotColor.g * attachmentColor.g; @@ -142,15 +144,15 @@ export class SkeletonRenderer { finalColor.b *= finalColor.a; } let darkColor = this.tempColor2; - if (!slot.darkColor) + if (!pose.darkColor) darkColor.set(0, 0, 0, 1.0); else { if (premultipliedAlpha) { - darkColor.r = slot.darkColor.r * finalColor.a; - darkColor.g = slot.darkColor.g * finalColor.a; - darkColor.b = slot.darkColor.b * finalColor.a; + darkColor.r = pose.darkColor.r * finalColor.a; + darkColor.g = pose.darkColor.g * finalColor.a; + darkColor.b = pose.darkColor.b * finalColor.a; } else { - darkColor.setFromColor(slot.darkColor); + darkColor.setFromColor(pose.darkColor); } darkColor.a = premultipliedAlpha ? 1.0 : 0.0; } @@ -197,7 +199,7 @@ export class SkeletonRenderer { } } - clipper.clipEndWithSlot(slot); + clipper.clipEnd(slot); } clipper.clipEnd(); }