Просмотр исходного кода

[ts] Port of physics constraints, SkeletonJson and SkeletonBinary incomplete.

Mario Zechner 1 год назад
Родитель
Сommit
c7aac73dee

+ 392 - 301
spine-ts/spine-core/src/Animation.ts

@@ -30,13 +30,15 @@
 import { VertexAttachment, Attachment } from "./attachments/Attachment.js";
 import { IkConstraint } from "./IkConstraint.js";
 import { PathConstraint } from "./PathConstraint.js";
-import { Skeleton } from "./Skeleton.js";
+import { Physics, 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";
 import { SequenceMode, SequenceModeValues } from "./attachments/Sequence.js";
+import { PhysicsConstraint } from "./PhysicsConstraint.js";
+import { PhysicsConstraintData } from "./PhysicsConstraintData.js";
 
 /** A simple container for a list of timelines and a name. */
 export class Animation {
@@ -150,7 +152,16 @@ const Property = {
 	pathConstraintSpacing: 17,
 	pathConstraintMix: 18,
 
-	sequence: 19
+	physicsConstraintInertia: 19,
+	physicsConstraintStrength: 20,
+	physicsConstraintDamping: 21,
+	physicsConstraintMass: 22,
+	physicsConstraintWind: 23,
+	physicsConstraintGravity: 24,
+	physicsConstraintMix: 25,
+	physicsConstraintReset: 26,
+
+	sequence: 27,
 }
 
 /** The interface for all timelines. */
@@ -335,6 +346,96 @@ export abstract class CurveTimeline1 extends CurveTimeline {
 		}
 		return this.getBezierValue(time, i, 1/*VALUE*/, curveType - 2/*BEZIER*/);
 	}
+
+	getRelativeValue (time: number, alpha: number, blend: MixBlend, current: number, setup: number) {
+		if (time < this.frames[0]) {
+			switch (blend) {
+			case MixBlend.setup:
+				return setup;
+			case MixBlend.first:
+				return current + (setup - current) * alpha;
+			}
+			return current;
+		}
+		let value = this.getCurveValue(time);
+		switch (blend) {
+		case MixBlend.setup:
+			return setup + value * alpha;
+		case MixBlend.first:
+		case MixBlend.replace:
+			value += setup - current;
+		}
+		return current + value * alpha;
+	}
+
+	getAbsoluteValue (time: number, alpha: number, blend: MixBlend, current: number, setup: number) {
+		if (time < this.frames[0]) {
+			switch (blend) {
+			case MixBlend.setup:
+				return setup;
+			case MixBlend.first:
+				return current + (setup - current) * alpha;
+			}
+			return current;
+		}
+		let value = this.getCurveValue(time);
+		if (blend == MixBlend.setup) return setup + (value - setup) * alpha;
+		return current + (value - current) * alpha;
+	}
+
+	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;
+			}
+			return current;
+		}
+		if (blend == MixBlend.setup) return setup + (value - setup) * alpha;
+		return current + (value - current) * alpha;
+	}
+
+	getScaleValue (time: number, alpha: number, blend: MixBlend, direction: MixDirection, current: number, setup: number) {
+		const frames = this.frames;
+		if (time < frames[0]) {
+			switch (blend) {
+			case MixBlend.setup:
+				return setup;
+			case MixBlend.first:
+				return current + (setup - current) * alpha;
+			}
+			return current;
+		}
+		let value = this.getCurveValue(time) * setup;
+		if (alpha == 1) {
+			if (blend == MixBlend.add) return current + value - setup;
+			return value;
+		}
+		// Mixing out uses sign of setup or current pose, else use sign of key.
+		if (direction == MixDirection.mixOut) {
+			switch (blend) {
+			case MixBlend.setup:
+				return setup + (Math.abs(value) * MathUtils.signum(setup) - setup) * alpha;
+			case MixBlend.first:
+			case MixBlend.replace:
+				return current + (Math.abs(value) * MathUtils.signum(current) - current) * alpha;
+			}
+		} else {
+			let s = 0;
+			switch (blend) {
+			case MixBlend.setup:
+				s = Math.abs(setup) * MathUtils.signum(value);
+				return s + (value - s) * alpha;
+			case MixBlend.first:
+			case MixBlend.replace:
+				s = Math.abs(current) * MathUtils.signum(value);
+				return s + (value - s) * alpha;
+			}
+		}
+		return current + (value - setup) * alpha;
+	}
 }
 
 /** The base class for a {@link CurveTimeline} which sets two properties. */
@@ -371,31 +472,7 @@ export class RotateTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event> | null, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.rotation = bone.data.rotation;
-					return;
-				case MixBlend.first:
-					bone.rotation += (bone.data.rotation - bone.rotation) * alpha;
-			}
-			return;
-		}
-
-		let r = this.getCurveValue(time);
-		switch (blend) {
-			case MixBlend.setup:
-				bone.rotation = bone.data.rotation + r * alpha;
-				break;
-			case MixBlend.first:
-			case MixBlend.replace:
-				r += bone.data.rotation - bone.rotation;
-			case MixBlend.add:
-				bone.rotation += r * alpha;
-		}
+		if (bone.active) bone.rotation = this.getRelativeValue(time, alpha, blend, bone.rotation, bone.data.rotation);
 	}
 }
 
@@ -478,32 +555,7 @@ export class TranslateXTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.x = bone.data.x;
-					return;
-				case MixBlend.first:
-					bone.x += (bone.data.x - bone.x) * alpha;
-			}
-			return;
-		}
-
-		let x = this.getCurveValue(time);
-		switch (blend) {
-			case MixBlend.setup:
-				bone.x = bone.data.x + x * alpha;
-				break;
-			case MixBlend.first:
-			case MixBlend.replace:
-				bone.x += (bone.data.x + x - bone.x) * alpha;
-				break;
-			case MixBlend.add:
-				bone.x += x * alpha;
-		}
+		if (bone.active) bone.x = this.getRelativeValue(time, alpha, blend, bone.x, bone.data.x);
 	}
 }
 
@@ -518,32 +570,7 @@ export class TranslateYTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.y = bone.data.y;
-					return;
-				case MixBlend.first:
-					bone.y += (bone.data.y - bone.y) * alpha;
-			}
-			return;
-		}
-
-		let y = this.getCurveValue(time);
-		switch (blend) {
-			case MixBlend.setup:
-				bone.y = bone.data.y + y * alpha;
-				break;
-			case MixBlend.first:
-			case MixBlend.replace:
-				bone.y += (bone.data.y + y - bone.y) * alpha;
-				break;
-			case MixBlend.add:
-				bone.y += y * alpha;
-		}
+		if (bone.active) bone.y = this.getRelativeValue(time, alpha, blend, bone.y, bone.data.y);
 	}
 }
 
@@ -664,59 +691,7 @@ export class ScaleXTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.scaleX = bone.data.scaleX;
-					return;
-				case MixBlend.first:
-					bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha;
-			}
-			return;
-		}
-
-		let x = this.getCurveValue(time) * bone.data.scaleX;
-		if (alpha == 1) {
-			if (blend == MixBlend.add)
-				bone.scaleX += x - bone.data.scaleX;
-			else
-				bone.scaleX = x;
-		} else {
-			// Mixing out uses sign of setup or current pose, else use sign of key.
-			let bx = 0;
-			if (direction == MixDirection.mixOut) {
-				switch (blend) {
-					case MixBlend.setup:
-						bx = bone.data.scaleX;
-						bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha;
-						break;
-					case MixBlend.first:
-					case MixBlend.replace:
-						bx = bone.scaleX;
-						bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha;
-						break;
-					case MixBlend.add:
-						bone.scaleX += (x - bone.data.scaleX) * alpha;
-				}
-			} else {
-				switch (blend) {
-					case MixBlend.setup:
-						bx = Math.abs(bone.data.scaleX) * MathUtils.signum(x);
-						bone.scaleX = bx + (x - bx) * alpha;
-						break;
-					case MixBlend.first:
-					case MixBlend.replace:
-						bx = Math.abs(bone.scaleX) * MathUtils.signum(x);
-						bone.scaleX = bx + (x - bx) * alpha;
-						break;
-					case MixBlend.add:
-						bone.scaleX += (x - bone.data.scaleX) * alpha;
-				}
-			}
-		}
+		if (bone.active) bone.scaleX = this.getScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.scaleX);
 	}
 }
 
@@ -731,59 +706,7 @@ export class ScaleYTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.scaleY = bone.data.scaleY;
-					return;
-				case MixBlend.first:
-					bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha;
-			}
-			return;
-		}
-
-		let y = this.getCurveValue(time) * bone.data.scaleY;
-		if (alpha == 1) {
-			if (blend == MixBlend.add)
-				bone.scaleY += y - bone.data.scaleY;
-			else
-				bone.scaleY = y;
-		} else {
-			// Mixing out uses sign of setup or current pose, else use sign of key.
-			let by = 0;
-			if (direction == MixDirection.mixOut) {
-				switch (blend) {
-					case MixBlend.setup:
-						by = bone.data.scaleY;
-						bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha;
-						break;
-					case MixBlend.first:
-					case MixBlend.replace:
-						by = bone.scaleY;
-						bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha;
-						break;
-					case MixBlend.add:
-						bone.scaleY += (y - bone.data.scaleY) * alpha;
-				}
-			} else {
-				switch (blend) {
-					case MixBlend.setup:
-						by = Math.abs(bone.data.scaleY) * MathUtils.signum(y);
-						bone.scaleY = by + (y - by) * alpha;
-						break;
-					case MixBlend.first:
-					case MixBlend.replace:
-						by = Math.abs(bone.scaleY) * MathUtils.signum(y);
-						bone.scaleY = by + (y - by) * alpha;
-						break;
-					case MixBlend.add:
-						bone.scaleY += (y - bone.data.scaleY) * alpha;
-				}
-			}
-		}
+		if (bone.active) bone.scaleY = this.getScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.scaleY);
 	}
 }
 
@@ -866,32 +789,7 @@ export class ShearXTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.shearX = bone.data.shearX;
-					return;
-				case MixBlend.first:
-					bone.shearX += (bone.data.shearX - bone.shearX) * alpha;
-			}
-			return;
-		}
-
-		let x = this.getCurveValue(time);
-		switch (blend) {
-			case MixBlend.setup:
-				bone.shearX = bone.data.shearX + x * alpha;
-				break;
-			case MixBlend.first:
-			case MixBlend.replace:
-				bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
-				break;
-			case MixBlend.add:
-				bone.shearX += x * alpha;
-		}
+		if (bone.active) bone.shearX = this.getRelativeValue(time, alpha, blend, bone.shearX, bone.data.shearX);
 	}
 }
 
@@ -906,32 +804,7 @@ export class ShearYTimeline extends CurveTimeline1 implements BoneTimeline {
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
 		let bone = skeleton.bones[this.boneIndex];
-		if (!bone.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					bone.shearY = bone.data.shearY;
-					return;
-				case MixBlend.first:
-					bone.shearY += (bone.data.shearY - bone.shearY) * alpha;
-			}
-			return;
-		}
-
-		let y = this.getCurveValue(time);
-		switch (blend) {
-			case MixBlend.setup:
-				bone.shearY = bone.data.shearY + y * alpha;
-				break;
-			case MixBlend.first:
-			case MixBlend.replace:
-				bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
-				break;
-			case MixBlend.add:
-				bone.shearY += y * alpha;
-		}
+		if (bone.active) bone.shearY = this.getRelativeValue(time, alpha, blend, bone.shearX, bone.data.shearY);
 	}
 }
 
@@ -1119,7 +992,7 @@ export class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
 		if (!slot.bone.active) return;
 
 		let color = slot.color;
-		if (time < this.frames[0]) { // Time is before first frame.
+		if (time < this.frames[0]) {
 			let setup = slot.data.color;
 			switch (blend) {
 				case MixBlend.setup:
@@ -1547,7 +1420,7 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline {
 		}
 
 		deform.length = vertexCount;
-		if (time >= frames[frames.length - 1]) { // Time is after last frame.
+		if (time >= frames[frames.length - 1]) {
 			let lastVertices = vertices[frames.length - 1];
 			if (alpha == 1) {
 				if (blend == MixBlend.add) {
@@ -1711,12 +1584,12 @@ export class EventTimeline extends Timeline {
 		let frames = this.frames;
 		let frameCount = this.frames.length;
 
-		if (lastTime > time) { // Fire events after last time for looped animations.
+		if (lastTime > time) { // Apply after lastTime for looped animations.
 			this.apply(skeleton, lastTime, Number.MAX_VALUE, firedEvents, alpha, blend, direction);
 			lastTime = -1;
 		} else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame.
 			return;
-		if (time < frames[0]) return; // Time is before first frame.
+		if (time < frames[0]) return;
 
 		let i = 0;
 		if (lastTime < frames[0])
@@ -1785,14 +1658,14 @@ 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 slot in {@link Skeleton#ikConstraints} that will be changed. */
-	ikConstraintIndex: number = 0;
+	/** The index of the IK constraint in {@link Skeleton#getIkConstraints()} that will be changed when this timeline is */
+	constraintIndex: number = 0;
 
 	constructor (frameCount: number, bezierCount: number, ikConstraintIndex: number) {
 		super(frameCount, bezierCount, [
 			Property.ikConstraint + "|" + ikConstraintIndex
 		]);
-		this.ikConstraintIndex = ikConstraintIndex;
+		this.constraintIndex = ikConstraintIndex;
 	}
 
 	getFrameEntries () {
@@ -1811,7 +1684,7 @@ export class IkConstraintTimeline extends CurveTimeline {
 	}
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
-		let constraint: IkConstraint = skeleton.ikConstraints[this.ikConstraintIndex];
+		let constraint: IkConstraint = skeleton.ikConstraints[this.constraintIndex];
 		if (!constraint.active) return;
 
 		let frames = this.frames;
@@ -1884,13 +1757,13 @@ export class IkConstraintTimeline extends CurveTimeline {
  * {@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. */
-	transformConstraintIndex: number = 0;
+	constraintIndex: number = 0;
 
 	constructor (frameCount: number, bezierCount: number, transformConstraintIndex: number) {
 		super(frameCount, bezierCount, [
 			Property.transformConstraint + "|" + transformConstraintIndex
 		]);
-		this.transformConstraintIndex = transformConstraintIndex;
+		this.constraintIndex = transformConstraintIndex;
 	}
 
 	getFrameEntries () {
@@ -1912,7 +1785,7 @@ export class TransformConstraintTimeline extends CurveTimeline {
 	}
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
-		let constraint: TransformConstraint = skeleton.transformConstraints[this.transformConstraintIndex];
+		let constraint: TransformConstraint = skeleton.transformConstraints[this.constraintIndex];
 		if (!constraint.active) return;
 
 		let frames = this.frames;
@@ -1996,85 +1869,52 @@ export class TransformConstraintTimeline extends CurveTimeline {
 
 /** Changes a path constraint's {@link PathConstraint#position}. */
 export class PathConstraintPositionTimeline extends CurveTimeline1 {
-	/** The index of the path constraint slot in {@link Skeleton#pathConstraints} that will be changed. */
-	pathConstraintIndex: number = 0;
+	/** The index of the path constraint in {@link Skeleton#getPathConstraints()} that will be changed when this timeline is
+	 * applied. */
+	constraintIndex: number = 0;
 
 	constructor (frameCount: number, bezierCount: number, pathConstraintIndex: number) {
 		super(frameCount, bezierCount, Property.pathConstraintPosition + "|" + pathConstraintIndex);
-		this.pathConstraintIndex = pathConstraintIndex;
+		this.constraintIndex = pathConstraintIndex;
 	}
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
-		let constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex];
-		if (!constraint.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					constraint.position = constraint.data.position;
-					return;
-				case MixBlend.first:
-					constraint.position += (constraint.data.position - constraint.position) * alpha;
-			}
-			return;
-		}
-
-		let position = this.getCurveValue(time);
-
-		if (blend == MixBlend.setup)
-			constraint.position = constraint.data.position + (position - constraint.data.position) * alpha;
-		else
-			constraint.position += (position - constraint.position) * alpha;
+		let constraint: PathConstraint = skeleton.pathConstraints[this.constraintIndex];
+		if (constraint.active)
+			constraint.position = this.getAbsoluteValue(time, alpha, blend, constraint.position, constraint.data.position);
 	}
 }
 
 /** Changes a path constraint's {@link PathConstraint#spacing}. */
 export class PathConstraintSpacingTimeline extends CurveTimeline1 {
-	/** The index of the path constraint slot in {@link Skeleton#getPathConstraints()} that will be changed. */
-	pathConstraintIndex = 0;
+	/** 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.pathConstraintIndex = pathConstraintIndex;
+		this.constraintIndex = pathConstraintIndex;
 	}
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
-		let constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex];
-		if (!constraint.active) return;
-
-		let frames = this.frames;
-		if (time < frames[0]) {
-			switch (blend) {
-				case MixBlend.setup:
-					constraint.spacing = constraint.data.spacing;
-					return;
-				case MixBlend.first:
-					constraint.spacing += (constraint.data.spacing - constraint.spacing) * alpha;
-			}
-			return;
-		}
-
-		let spacing = this.getCurveValue(time);
-
-		if (blend == MixBlend.setup)
-			constraint.spacing = constraint.data.spacing + (spacing - constraint.data.spacing) * alpha;
-		else
-			constraint.spacing += (spacing - constraint.spacing) * alpha;
+		let constraint: PathConstraint = skeleton.pathConstraints[this.constraintIndex];
+		if (constraint.active)
+			constraint.spacing = this.getAbsoluteValue(time, alpha, blend, constraint.spacing, constraint.data.spacing);
 	}
 }
 
 /** 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 slot in {@link Skeleton#getPathConstraints()} that will be changed. */
-	pathConstraintIndex = 0;
+	/** 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.pathConstraintMix + "|" + pathConstraintIndex
 		]);
-		this.pathConstraintIndex = pathConstraintIndex;
+		this.constraintIndex = pathConstraintIndex;
 	}
 
 	getFrameEntries () {
@@ -2091,7 +1931,7 @@ export class PathConstraintMixTimeline extends CurveTimeline {
 	}
 
 	apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
-		let constraint: PathConstraint = skeleton.pathConstraints[this.pathConstraintIndex];
+		let constraint: PathConstraint = skeleton.pathConstraints[this.constraintIndex];
 		if (!constraint.active) return;
 
 		let frames = this.frames;
@@ -2148,6 +1988,257 @@ export class PathConstraintMixTimeline extends CurveTimeline {
 	}
 }
 
+/** 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;
+	}
+
+	apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
+		let constraint: PhysicsConstraint;
+		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));
+			}
+		} else {
+			constraint = skeleton.physicsConstraints[this.constraintIndex];
+			if (constraint.active) this.set(constraint, this.getAbsoluteValue(time, alpha, blend, this.get(constraint), this.setup(constraint)));
+		}
+	}
+
+	abstract setup (constraint: PhysicsConstraint): number;
+
+	abstract get (constraint: PhysicsConstraint): number;
+
+	abstract set (constraint: PhysicsConstraint, value: number): void;
+
+	abstract global (constraint: PhysicsConstraintData): boolean;
+}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getInertia()}. */
+	export class PhysicsConstraintInertiaTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintInertia);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return constraint.data.inertia;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return constraint.inertia;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.inertia = value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.inertiaGlobal;
+		}
+	}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getStrength()}. */
+	export class PhysicsConstraintStrengthTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintStrength);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return constraint.data.strength;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return constraint.strength;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.strength = value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.strengthGlobal;
+		}
+	}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getDamping()}. */
+	export class PhysicsConstraintDampingTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintDamping);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return constraint.data.damping;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return constraint.damping;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.damping = value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.dampingGlobal;
+		}
+	}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getMassInverse()}. The timeline values are not inverted. */
+	export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintMass);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return 1 / constraint.data.massInverse;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return 1 / constraint.massInverse;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.massInverse = 1 / value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.massGlobal;
+		}
+	}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getWind()}. */
+	export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintWind);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return constraint.data.wind;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return constraint.wind;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.wind = value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.windGlobal;
+		}
+	}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getGravity()}. */
+	export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintGravity);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return constraint.data.gravity;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return constraint.gravity;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.gravity = value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.gravityGlobal;
+		}
+	}
+
+	/** Changes a physics constraint's {@link PhysicsConstraint#getMix()}. */
+	export class PhysicsConstraintMixTimeline extends PhysicsConstraintTimeline {
+		constructor (frameCount: number, bezierCount: number, physicsConstraintIndex: number, property: number) {
+			super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintMix);
+		}
+
+		setup (constraint: PhysicsConstraint): number {
+			return constraint.data.mix;
+		}
+
+		get (constraint: PhysicsConstraint): number {
+			return constraint.mix;
+		}
+
+		set (constraint: PhysicsConstraint, value: number): void {
+			constraint.mix = value;
+		}
+
+		global (constraint: PhysicsConstraintData): boolean {
+			return constraint.mixGlobal;
+		}
+	}
+
+	/** Resets a physics constraint when specific animation times are reached. */
+	export class PhysicsConstraintResetTimeline extends Timeline {
+		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
+		* applied, or -1 if all physics constraints in the skeleton will be reset. */
+		constraintIndex: number;
+
+		/** @param physicsConstraintIndex -1 for all physics constraints in the skeleton. */
+		constructor (frameCount: number, physicsConstraintIndex: number) {
+			super(frameCount, PhysicsConstraintResetTimeline.propertyIds);
+			this.constraintIndex = physicsConstraintIndex;
+		}
+
+		getFrameCount () {
+			return this.frames.length;
+		}
+
+		/** Sets the time for the specified frame.
+		 * @param frame Between 0 and <code>frameCount</code>, inclusive. */
+		setFrame (frame: number, time: number) {
+			this.frames[frame] = time;
+		}
+
+		/** Resets the physics constraint when frames > <code>lastTime</code> and <= <code>time</code>. */
+		apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
+
+			let constraint: PhysicsConstraint | undefined;
+			if (this.constraintIndex != -1) {
+				constraint = skeleton.physicsConstraints[this.constraintIndex];
+				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);
+				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 (constraint != null)
+					constraint.reset();
+				else {
+					for (const constraint of skeleton.physicsConstraints) {
+						if (constraint.active) constraint.reset();
+					}
+				}
+			}
+		}
+	}
+
 /** Changes a slot's {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */
 export class SequenceTimeline extends Timeline implements SlotTimeline {
 	static ENTRIES = 3;
@@ -2199,7 +2290,7 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
 		}
 
 		let frames = this.frames;
-		if (time < frames[0]) { // Time is before first frame.
+		if (time < frames[0]) {
 			if (blend == MixBlend.setup || blend == MixBlend.first) slot.sequenceIndex = -1;
 			return;
 		}

+ 47 - 22
spine-ts/spine-core/src/AnimationState.ts

@@ -173,11 +173,13 @@ export class AnimationState {
 			let blend: MixBlend = i == 0 ? MixBlend.first : current.mixBlend;
 
 			// Apply mixing from entries first.
-			let mix = current.alpha;
+			let alpha = current.alpha;
 			if (current.mixingFrom)
-				mix *= this.applyMixingFrom(current, skeleton, blend);
+				alpha *= this.applyMixingFrom(current, skeleton, blend);
 			else if (current.trackTime >= current.trackEnd && !current.next)
-				mix = 0;
+				alpha = 0;
+			let attachments = alpha >= current.alphaAttachmentThreshold;
+
 
 			// Apply current entry.
 			let animationLast = current.animationLast, animationTime = current.getAnimationTime(), applyTime = animationTime;
@@ -188,17 +190,18 @@ export class AnimationState {
 			}
 			let timelines = current.animation!.timelines;
 			let timelineCount = timelines.length;
-			if ((i == 0 && mix == 1) || blend == MixBlend.add) {
+			if ((i == 0 && alpha == 1) || blend == MixBlend.add) {
+				if (i == 0) attachments = true;
 				for (let ii = 0; ii < timelineCount; ii++) {
 					// Fixes issue #302 on IOS9 where mix, blend sometimes became undefined and caused assets
 					// to sometimes stop rendering when using color correction, as their RGBA values become NaN.
 					// (https://github.com/pixijs/pixi-spine/issues/302)
-					Utils.webkit602BugfixHelper(mix, blend);
+					Utils.webkit602BugfixHelper(alpha, blend);
 					var timeline = timelines[ii];
 					if (timeline instanceof AttachmentTimeline)
-						this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, true);
+						this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments);
 					else
-						timeline.apply(skeleton, animationLast, applyTime, applyEvents, mix, blend, MixDirection.mixIn);
+						timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.mixIn);
 				}
 			} else {
 				let timelineMode = current.timelineMode;
@@ -211,13 +214,13 @@ export class AnimationState {
 					let timeline = timelines[ii];
 					let timelineBlend = timelineMode[ii] == SUBSEQUENT ? blend : MixBlend.setup;
 					if (!shortestRotation && timeline instanceof RotateTimeline) {
-						this.applyRotateTimeline(timeline, skeleton, applyTime, mix, timelineBlend, current.timelinesRotation, ii << 1, firstFrame);
+						this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, current.timelinesRotation, ii << 1, firstFrame);
 					} else if (timeline instanceof AttachmentTimeline) {
-						this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, true);
+						this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments);
 					} else {
 						// This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109
-						Utils.webkit602BugfixHelper(mix, blend);
-						timeline.apply(skeleton, animationLast, applyTime, applyEvents, mix, timelineBlend, MixDirection.mixIn);
+						Utils.webkit602BugfixHelper(alpha, blend);
+						timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.mixIn);
 					}
 				}
 			}
@@ -259,7 +262,7 @@ export class AnimationState {
 			if (blend != MixBlend.first) blend = from.mixBlend;
 		}
 
-		let attachments = mix < from.attachmentThreshold, drawOrder = mix < from.drawOrderThreshold;
+		let attachments = mix < from.mixAttachmentThreshold, drawOrder = mix < from.mixDrawOrderThreshold;
 		let timelines = from.animation!.timelines;
 		let timelineCount = timelines.length;
 		let alphaHold = from.alpha * to.interruptAlpha, alphaMix = alphaHold * (1 - mix);
@@ -316,7 +319,7 @@ export class AnimationState {
 				if (!shortestRotation && timeline instanceof RotateTimeline)
 					this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, from.timelinesRotation, i << 1, firstFrame);
 				else if (timeline instanceof AttachmentTimeline)
-					this.applyAttachmentTimeline(timeline, skeleton, applyTime, timelineBlend, attachments);
+					this.applyAttachmentTimeline(timeline, skeleton, applyTime, timelineBlend, attachments && alpha >= from.alphaAttachmentThreshold);
 				else {
 					// This fixes the WebKit 602 specific issue described at http://esotericsoftware.com/forum/iOS-10-disappearing-graphics-10109
 					Utils.webkit602BugfixHelper(alpha, blend);
@@ -385,7 +388,7 @@ export class AnimationState {
 
 		// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
 		let total = 0, diff = r2 - r1;
-		diff -= (16384 - ((16384.499999999996 - diff / 360) | 0)) * 360;
+		diff -= Math.ceil(diff / 360 - 0.5) * 360;
 		if (diff == 0) {
 			total = timelinesRotation[i];
 		} else {
@@ -661,8 +664,9 @@ export class AnimationState {
 		entry.shortestRotation = false;
 
 		entry.eventThreshold = 0;
-		entry.attachmentThreshold = 0;
-		entry.drawOrderThreshold = 0;
+		entry.alphaAttachmentThreshold = 0;
+		entry.mixAttachmentThreshold = 0;
+		entry.mixDrawOrderThreshold = 0;
 
 		entry.animationStart = 0;
 		entry.animationEnd = animation.duration;
@@ -843,12 +847,16 @@ export class TrackEntry {
 	/** When the mix percentage ({@link #mixtime} / {@link #mixDuration}) is less than the
 	 * `attachmentThreshold`, attachment timelines are applied while this animation is being mixed out. Defaults to
 	 * 0, so attachment timelines are not applied while this animation is being mixed out. */
-	attachmentThreshold: number = 0;
+	mixAttachmentThreshold: number = 0;
 
-	/** When the mix percentage ({@link #mixTime} / {@link #mixDuration}) is less than the
-	 * `drawOrderThreshold`, draw order timelines are applied while this animation is being mixed out. Defaults to 0,
-	 * so draw order timelines are not applied while this animation is being mixed out. */
-	drawOrderThreshold: number = 0;
+	/** When {@link #getAlpha()} is greater than <code>alphaAttachmentThreshold</code>, attachment timelines are applied.
+	 * Defaults to 0, so attachment timelines are always applied. */
+	alphaAttachmentThreshold: number = 0;
+
+	/** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the
+	 * <code>mixDrawOrderThreshold</code>, draw order timelines are applied while this animation is being mixed out. Defaults to
+	 * 0, so draw order timelines are not applied while this animation is being mixed out. */
+	mixDrawOrderThreshold: number = 0;
 
 	/** Seconds when this animation starts, both initially and after looping. Defaults to 0.
 	 *
@@ -930,7 +938,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; interruptAlpha: number = 0; totalAlpha: number = 0;
+
+	get mixDuration () {
+		return this._mixDuration;
+	}
+
+	set mixDuration (mixDuration: number) {
+		this.mixDuration = mixDuration;
+		if (this.previous != null && this.delay <= 0) this.delay += this.previous.getTrackComplete() - mixDuration;
+		this.delay = this.delay;
+	}
 
 	/** Controls how properties keyed in the animation are mixed with lower tracks. Defaults to {@link MixBlend#replace}, which
 	 * replaces the values from the lower tracks with the animation values. {@link MixBlend#add} adds the animation values to
@@ -998,6 +1016,13 @@ export class TrackEntry {
 		}
 		return this.trackTime; // Next update.
 	}
+
+	/** Returns true if this track entry has been applied at least once.
+	 * <p>
+	 * See {@link AnimationState#apply(Skeleton)}. */
+	wasApplied () {
+		return this.nextTrackLast != -1;
+	}
 }
 
 export class EventQueue {

+ 80 - 62
spine-ts/spine-core/src/Bone.ts

@@ -28,7 +28,7 @@
  *****************************************************************************/
 
 import { BoneData, TransformMode } from "./BoneData.js";
-import { Skeleton } from "./Skeleton.js";
+import { Physics, Skeleton } from "./Skeleton.js";
 import { Updatable } from "./Updatable.js";
 import { MathUtils, Vector2 } from "./Utils.js";
 
@@ -130,7 +130,7 @@ export class Bone implements Updatable {
 	}
 
 	/** Computes the world transform using the parent bone and this bone's local applied transform. */
-	update () {
+	update (physics: Physics) {
 		this.updateWorldTransformWith(this.ax, this.ay, this.arotation, this.ascaleX, this.ascaleY, this.ashearX, this.ashearY);
 	}
 
@@ -158,13 +158,13 @@ export class Bone implements Updatable {
 		let parent = this.parent;
 		if (!parent) { // Root bone.
 			let skeleton = this.skeleton;
-			let rotationY = rotation + 90 + shearY;
-			let sx = skeleton.scaleX;
-			let sy = skeleton.scaleY;
-			this.a = MathUtils.cosDeg(rotation + shearX) * scaleX * sx;
-			this.b = MathUtils.cosDeg(rotationY) * scaleY * sx;
-			this.c = MathUtils.sinDeg(rotation + shearX) * scaleX * sy;
-			this.d = MathUtils.sinDeg(rotationY) * scaleY * sy;
+			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;
@@ -176,11 +176,12 @@ export class Bone implements Updatable {
 
 		switch (this.data.transformMode) {
 			case TransformMode.Normal: {
-				let rotationY = rotation + 90 + shearY;
-				let la = MathUtils.cosDeg(rotation + shearX) * scaleX;
-				let lb = MathUtils.cosDeg(rotationY) * scaleY;
-				let lc = MathUtils.sinDeg(rotation + shearX) * scaleX;
-				let ld = MathUtils.sinDeg(rotationY) * scaleY;
+				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;
@@ -188,11 +189,12 @@ export class Bone implements Updatable {
 				return;
 			}
 			case TransformMode.OnlyTranslation: {
-				let rotationY = rotation + 90 + shearY;
-				this.a = MathUtils.cosDeg(rotation + shearX) * scaleX;
-				this.b = MathUtils.cosDeg(rotationY) * scaleY;
-				this.c = MathUtils.sinDeg(rotation + shearX) * scaleX;
-				this.d = MathUtils.sinDeg(rotationY) * scaleY;
+				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 TransformMode.NoRotationOrReflection: {
@@ -210,12 +212,12 @@ export class Bone implements Updatable {
 					pc = 0;
 					prx = 90 - Math.atan2(pd, pb) * MathUtils.radDeg;
 				}
-				let rx = rotation + shearX - prx;
-				let ry = rotation + shearY - prx + 90;
-				let la = MathUtils.cosDeg(rx) * scaleX;
-				let lb = MathUtils.cosDeg(ry) * scaleY;
-				let lc = MathUtils.sinDeg(rx) * scaleX;
-				let ld = MathUtils.sinDeg(ry) * scaleY;
+				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;
@@ -224,8 +226,8 @@ export class Bone implements Updatable {
 			}
 			case TransformMode.NoScale:
 			case TransformMode.NoScaleOrReflection: {
-				let cos = MathUtils.cosDeg(rotation);
-				let sin = MathUtils.sinDeg(rotation);
+				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);
@@ -235,13 +237,15 @@ export class Bone implements Updatable {
 				s = Math.sqrt(za * za + zc * zc);
 				if (this.data.transformMode == TransformMode.NoScale
 					&& (pa * pd - pb * pc < 0) != (this.skeleton.scaleX < 0 != this.skeleton.scaleY < 0)) s = -s;
-				let r = Math.PI / 2 + Math.atan2(zc, za);
-				let zb = Math.cos(r) * s;
-				let zd = Math.sin(r) * s;
-				let la = MathUtils.cosDeg(shearX) * scaleX;
-				let lb = MathUtils.cosDeg(90 + shearY) * scaleY;
-				let lc = MathUtils.sinDeg(shearX) * scaleX;
-				let ld = MathUtils.sinDeg(90 + shearY) * scaleY;
+				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;
@@ -267,26 +271,6 @@ export class Bone implements Updatable {
 		this.shearY = data.shearY;
 	}
 
-	/** 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);
-	}
-
 	/** 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
@@ -374,6 +358,27 @@ export class Bone implements Updatable {
 		}
 	}
 
+
+	/** 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);
@@ -391,6 +396,18 @@ export class Bone implements Updatable {
 		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);
@@ -406,14 +423,15 @@ export class Bone implements Updatable {
 
 	/** Rotates the world transform the specified amount.
 	 * <p>
-	 * After changes are made to the world transform, {@link #updateAppliedTransform()} should be called and {@link #update()} will
-	 * need to be called on any child bones, recursively. */
+	 * 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) {
-		let a = this.a, b = this.b, c = this.c, d = this.d;
-		let cos = MathUtils.cosDeg(degrees), sin = MathUtils.sinDeg(degrees);
-		this.a = cos * a - sin * c;
-		this.b = cos * b - sin * d;
-		this.c = sin * a + cos * c;
-		this.d = sin * b + cos * d;
+		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;
 	}
 }

+ 7 - 1
spine-ts/spine-core/src/BoneData.ts

@@ -49,7 +49,7 @@ export class BoneData {
 	/** The local y translation. */
 	y = 0;
 
-	/** The local rotation. */
+	/** The local rotation in degrees, counter clockwise. */
 	rotation = 0;
 
 	/** The local scaleX. */
@@ -76,6 +76,12 @@ export class BoneData {
 	 * rendered at runtime. */
 	color = new Color();
 
+	/** The bone icon as it was in Spine, or null if nonessential data was not exported. */
+	icon?: string;
+
+	/** False if the bone was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
+	visible = false;
+
 	constructor (index: number, name: string, parent: BoneData | null) {
 		if (index < 0) throw new Error("index must be >= 0.");
 		if (!name) throw new Error("name cannot be null.");

+ 19 - 7
spine-ts/spine-core/src/IkConstraint.ts

@@ -30,7 +30,7 @@
 import { Bone } from "./Bone.js";
 import { TransformMode } from "./BoneData.js";
 import { IkConstraintData } from "./IkConstraintData.js";
-import { Skeleton } from "./Skeleton.js";
+import { Physics, Skeleton } from "./Skeleton.js";
 import { Updatable } from "./Updatable.js";
 import { MathUtils } from "./Utils.js";
 
@@ -90,7 +90,16 @@ export class IkConstraint implements Updatable {
 		return this.active;
 	}
 
-	update () {
+	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;
 		let bones = this.bones;
@@ -149,11 +158,14 @@ export class IkConstraint implements Updatable {
 					tx = targetX - bone.worldX;
 					ty = targetY - bone.worldY;
 			}
-			let b = bone.data.length * sx, dd = Math.sqrt(tx * tx + ty * ty);
-			if ((compress && dd < b) || (stretch && dd > b) && b > 0.0001) {
-				let s = (dd / b - 1) * alpha + 1;
-				sx *= s;
-				if (uniform) sy *= s;
+			const b = bone.data.length * sx;
+			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;
+				}
 			}
 		}
 		bone.updateWorldTransformWith(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX,

+ 13 - 8
spine-ts/spine-core/src/PathConstraint.ts

@@ -30,7 +30,7 @@
 import { PathAttachment } from "./attachments/PathAttachment.js";
 import { Bone } from "./Bone.js";
 import { PathConstraintData, RotateMode, SpacingMode, PositionMode } from "./PathConstraintData.js";
-import { Skeleton } from "./Skeleton.js";
+import { Physics, Skeleton } from "./Skeleton.js";
 import { Slot } from "./Slot.js";
 import { Updatable } from "./Updatable.js";
 import { Utils, MathUtils } from "./Utils.js";
@@ -95,7 +95,16 @@ export class PathConstraint implements Updatable {
 		return this.active;
 	}
 
-	update () {
+	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.target.getAttachment();
 		if (!(attachment instanceof PathAttachment)) return;
 
@@ -116,12 +125,8 @@ export class PathConstraint implements Updatable {
 					for (let i = 0, n = spacesCount - 1; i < n; i++) {
 						let bone = bones[i];
 						let setupLength = bone.data.length;
-						if (setupLength < PathConstraint.epsilon)
-							lengths[i] = 0;
-						else {
-							let x = setupLength * bone.a, y = setupLength * bone.c;
-							lengths[i] = Math.sqrt(x * x + y * y);
-						}
+						let x = setupLength * bone.a, y = setupLength * bone.c;
+						lengths[i] = Math.sqrt(x * x + y * y);
 					}
 				}
 				Utils.arrayFill(spaces, 1, spacesCount, spacing);

+ 270 - 0
spine-ts/spine-core/src/PhysicsConstraint.ts

@@ -0,0 +1,270 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, 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.js";
+import { PhysicsConstraintData } from "./PhysicsConstraintData.js";
+import { Physics, Skeleton } from "./Skeleton.js";
+import { Updatable } from "./Updatable.js";
+import { MathUtils } from "./Utils.js";
+
+
+/** Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
+export class PhysicsConstraint implements Updatable {
+	readonly data: PhysicsConstraintData;
+	private _bone: Bone | null = null;
+	/** The bone constrained by this physics constraint. */
+	public set bone (bone: Bone) { this._bone = bone; }
+	public get bone () {
+		if (!this._bone) throw new Error("Bone not set.")
+		else return this._bone;
+	}
+	inertia = 0;
+	strength = 0;
+	damping = 0;
+	massInverse = 0;
+	wind = 0;
+	gravity = 0;
+	mix = 0;
+
+	_reset = true;
+	ux = 0;
+	uy = 0;
+	cx = 0;
+	cy = 0;
+	tx = 0;
+	ty = 0;
+	xOffset = 0;
+	xVelocity = 0;
+	yOffset = 0;
+	yVelocity = 0;
+	rotateOffset = 0;
+	rotateVelocity = 0;
+	scaleOffset = 0
+	scaleVelocity = 0;
+
+	active = false;
+
+	readonly skeleton: Skeleton;
+	remaining = 0;
+	lastTime = 0;
+
+	constructor(data: PhysicsConstraintData, skeleton: Skeleton) {
+		this.data = data;
+		this.skeleton = skeleton;
+		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;
+	}
+
+	reset () {
+		this.remaining = 0;
+		this.lastTime = this.skeleton.time;
+		this._reset = true;
+		this.xOffset = 0;
+		this.xVelocity = 0;
+		this.yOffset = 0;
+		this.yVelocity = 0;
+		this.rotateOffset = 0;
+		this.rotateVelocity = 0;
+		this.scaleOffset = 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;
+	}
+
+	isActive () {
+		return this.active;
+	}
+
+	/** Applies the constraint to the constrained bones. */
+	update (physics: Physics) {
+		const mix = this.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;
+
+		switch (physics) {
+		case Physics.none:
+			return;
+		case Physics.reset:
+			this.reset();
+			// Fall through.
+		case Physics.update:
+			this.remaining += Math.max(this.skeleton.time - this.lastTime, 0);
+			this.lastTime = this.skeleton.time;
+
+			const bx = bone.worldX, by = bone.worldY;
+			if (this._reset) {
+				this._reset = false;
+				this.ux = bx;
+				this.uy = by;
+			} else {
+				let remaining = this.remaining, i = this.inertia, step = this.data.step;
+				if (x || y) {
+					if (x) {
+						this.xOffset += (this.ux - bx) * i;
+						this.ux = bx;
+					}
+					if (y) {
+						this.yOffset += (this.uy - by) * i;
+						this.uy = by;
+					}
+					if (remaining >= step) {
+						const m = this.massInverse * step, e = this.strength, w = this.wind * 100, g = this.gravity * -100;
+						const d = Math.pow(this.damping, 60 * step);
+						do {
+							if (x) {
+								this.xVelocity += (w - this.xOffset * e) * m;
+								this.xOffset += this.xVelocity * step;
+								this.xVelocity *= d;
+							}
+							if (y) {
+								this.yVelocity += (g - this.yOffset * e) * m;
+								this.yOffset += this.yVelocity * step;
+								this.yVelocity *= d;
+							}
+							remaining -= step;
+						} while (remaining >= step);
+					}
+					if (x) bone.worldX += this.xOffset * mix * this.data.x;
+					if (y) bone.worldY += this.yOffset * mix * this.data.y;
+				}
+				if (rotateOrShearX || scaleX) {
+					let ca = Math.atan2(bone.c, bone.a), c = 0, s = 0, mr = 0;
+					if (rotateOrShearX) {
+						mr = mix * this.data.rotate;
+						let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY, r = Math.atan2(dy + this.ty, dx + this.tx) - ca - this.rotateOffset * mr;
+						this.rotateOffset += (r - Math.ceil(r * MathUtils.invPI2 - 0.5) * MathUtils.PI2) * i;
+						r = this.rotateOffset * mr + ca;
+						c = Math.cos(r);
+						s = Math.sin(r);
+						if (scaleX) {
+							r = l * bone.getWorldScaleX();
+							if (r > 0) this.scaleOffset += (dx * c + dy * s) * i / r;
+						}
+					} else {
+						c = Math.cos(ca);
+						s = Math.sin(ca);
+						const r = l * bone.getWorldScaleX();
+						if (r > 0) this.scaleOffset += ((this.cx - bone.worldX) * c + (this.cy - bone.worldY) * s) * i / r;
+					}
+					remaining = this.remaining;
+					if (remaining >= step) {
+						const m = this.massInverse * step, e = this.strength, w = this.wind, g = this.gravity;
+						const d = Math.pow(this.damping, 60 * step);
+						while (true) {
+							remaining -= step;
+							if (scaleX) {
+								this.scaleVelocity += (w * c - g * s - this.scaleOffset * e) * m;
+								this.scaleOffset += this.scaleVelocity * step;
+								this.scaleVelocity *= d;
+							}
+							if (rotateOrShearX) {
+								this.rotateVelocity += (-0.01 * l * (w * s + g * c) - this.rotateOffset * e) * m;
+								this.rotateOffset += this.rotateVelocity * step;
+								this.rotateVelocity *= d;
+								if (remaining < step) break;
+								const r = this.rotateOffset * mr + ca;
+								c = Math.cos(r);
+								s = Math.sin(r);
+							} else if (remaining < step) //
+								break;
+						}
+					}
+				}
+				this.remaining = remaining;
+			}
+			this.cx = bone.worldX;
+			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;
+		}
+
+		if (rotateOrShearX) {
+			let o = this.rotateOffset * mix, s = 0, c = 0, a = 0;
+			if (this.data.shearX > 0) {
+				let r = 0;
+				if (this.data.rotate > 0) {
+					r = o * this.data.rotate;
+					s = Math.sin(r);
+					c = Math.cos(r);
+					a = bone.b;
+					bone.b = c * a - s * bone.d;
+					bone.d = s * a + c * bone.d;
+				}
+				r += o * this.data.shearX;
+				s = Math.sin(r);
+				c = Math.cos(r);
+				a = bone.a;
+				bone.a = c * a - s * bone.c;
+				bone.c = s * a + c * bone.c;
+			} else {
+				o *= this.data.rotate;
+				s = Math.sin(o);
+				c = Math.cos(o);
+				a = bone.a;
+				bone.a = c * a - s * bone.c;
+				bone.c = s * a + c * bone.c;
+				a = bone.b;
+				bone.b = c * a - s * bone.d;
+				bone.d = s * a + c * bone.d;
+			}
+		}
+		if (scaleX) {
+			const s = 1 + this.scaleOffset * mix * this.data.scaleX;
+			bone.a *= s;
+			bone.c *= s;
+		}
+		if (physics != Physics.pose) {
+			this.tx = l * bone.a;
+			this.ty = l * bone.c;
+		}
+		bone.updateAppliedTransform();
+	}
+}

+ 71 - 0
spine-ts/spine-core/src/PhysicsConstraintData.ts

@@ -0,0 +1,71 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated July 28, 2023. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2023, 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 { BoneData } from "./BoneData.js";
+import { ConstraintData } from "./ConstraintData.js";
+
+
+/** Stores the setup pose for a {@link PhysicsConstraint}.
+ * <p>
+ * See <a href="http://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
+export class  PhysicsConstraintData extends ConstraintData {
+	private _bone: BoneData | null = null;
+	/** 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;
+	}
+
+	x = 0;
+	y = 0;
+	rotate = 0;
+	scaleX = 1;
+	shearX = 1;
+	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;
+	massGlobal = false;
+	windGlobal = false;
+	gravityGlobal = false;
+	mixGlobal = false;
+
+	constructor (name: string) {
+		super(name, 0, false);
+	}
+}

+ 87 - 65
spine-ts/spine-core/src/Skeleton.ts

@@ -34,6 +34,7 @@ import { RegionAttachment } from "./attachments/RegionAttachment.js";
 import { Bone } from "./Bone.js";
 import { IkConstraint } from "./IkConstraint.js";
 import { PathConstraint } from "./PathConstraint.js";
+import { PhysicsConstraint } from "./PhysicsConstraint.js";
 import { SkeletonData } from "./SkeletonData.js";
 import { Skin } from "./Skin.js";
 import { Slot } from "./Slot.js";
@@ -68,6 +69,10 @@ export class Skeleton {
 	/** The skeleton's path constraints. */
 	pathConstraints: Array<PathConstraint>;
 
+
+	/** The skeleton's physics constraints. */
+	physicsConstraints: Array<PhysicsConstraint>;
+
 	/** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */
 	_updateCache = new Array<Updatable>();
 
@@ -99,6 +104,11 @@ export class Skeleton {
 	/** Sets the skeleton Y position, which is added to the root bone worldY position. */
 	y = 0;
 
+	/** Returns the skeleton's time. This is used for time-based manipulations, such as {@link PhysicsConstraint}.
+	 * <p>
+	 * See {@link #update(float)}. */
+	time = 0;
+
 	constructor (data: SkeletonData) {
 		if (!data) throw new Error("data cannot be null.");
 		this.data = data;
@@ -145,6 +155,12 @@ export class Skeleton {
 			this.pathConstraints.push(new PathConstraint(pathConstraintData, this));
 		}
 
+		this.physicsConstraints = new Array<PhysicsConstraint>();
+		for (let i = 0; i < data.physicsConstraints.length; i++) {
+			let physicsConstraintData = data.physicsConstraints[i];
+			this.physicsConstraints.push(new PhysicsConstraint(physicsConstraintData, this));
+		}
+
 		this.color = new Color(1, 1, 1, 1);
 		this.updateCache();
 	}
@@ -178,8 +194,9 @@ export class Skeleton {
 		let ikConstraints = this.ikConstraints;
 		let transformConstraints = this.transformConstraints;
 		let pathConstraints = this.pathConstraints;
-		let ikCount = ikConstraints.length, transformCount = transformConstraints.length, pathCount = pathConstraints.length;
-		let constraintCount = ikCount + transformCount + pathCount;
+		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++) {
@@ -204,6 +221,13 @@ export class Skeleton {
 					continue outer;
 				}
 			}
+			for (let ii = 0; ii < physicsCount; ii++) {
+				const constraint = physicsConstraints[ii];
+				if (constraint.data.order == i) {
+					this.sortPhysicsConstraint(constraint);
+					continue outer;
+				}
+			}
 		}
 
 		for (let i = 0, n = bones.length; i < n; i++)
@@ -316,6 +340,22 @@ export class Skeleton {
 		}
 	}
 
+	sortPhysicsConstraint (constraint: PhysicsConstraint) {
+		constraint.active = !constraint.data.skinRequired || (this.skin != null && Utils.contains(this.skin.constraints, constraint.data, true));
+		if (!constraint.active) return;
+
+		const bone = constraint.bone;
+		constraint.active = bone.active;
+		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;
@@ -338,7 +378,7 @@ export class Skeleton {
 	 *
 	 * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine
 	 * Runtimes Guide. */
-	updateWorldTransform () {
+	updateWorldTransform (physics: Physics) {
 		let bones = this.bones;
 		for (let i = 0, n = bones.length; i < n; i++) {
 			let bone = bones[i];
@@ -353,10 +393,10 @@ export class Skeleton {
 
 		let updateCache = this._updateCache;
 		for (let i = 0, n = updateCache.length; i < n; i++)
-			updateCache[i].update();
+			updateCache[i].update(physics);
 	}
 
-	updateWorldTransformWith (parent: Bone) {
+	updateWorldTransformWith (physics: Physics, parent: Bone) {
 		// 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.");
@@ -364,11 +404,12 @@ export class Skeleton {
 		rootBone.worldX = pa * this.x + pb * this.y + parent.worldX;
 		rootBone.worldY = pc * this.x + pd * this.y + parent.worldY;
 
-		let rotationY = rootBone.rotation + 90 + rootBone.shearY;
-		let la = MathUtils.cosDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
-		let lb = MathUtils.cosDeg(rotationY) * rootBone.scaleY;
-		let lc = MathUtils.sinDeg(rootBone.rotation + rootBone.shearX) * rootBone.scaleX;
-		let ld = MathUtils.sinDeg(rotationY) * rootBone.scaleY;
+		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;
@@ -378,7 +419,7 @@ export class Skeleton {
 		let updateCache = this._updateCache;
 		for (let i = 0, n = updateCache.length; i < n; i++) {
 			let updatable = updateCache[i];
-			if (updatable != rootBone) updatable.update();
+			if (updatable != rootBone) updatable.update(physics);
 		}
 	}
 
@@ -390,42 +431,11 @@ export class Skeleton {
 
 	/** Sets the bones and constraints to their setup pose values. */
 	setBonesToSetupPose () {
-		let bones = this.bones;
-		for (let i = 0, n = bones.length; i < n; i++)
-			bones[i].setToSetupPose();
-
-		let ikConstraints = this.ikConstraints;
-		for (let i = 0, n = ikConstraints.length; i < n; i++) {
-			let constraint = ikConstraints[i];
-			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;
-		}
-
-		let transformConstraints = this.transformConstraints;
-		for (let i = 0, n = transformConstraints.length; i < n; i++) {
-			let constraint = transformConstraints[i];
-			let data = constraint.data;
-			constraint.mixRotate = data.mixRotate;
-			constraint.mixX = data.mixX;
-			constraint.mixY = data.mixY;
-			constraint.mixScaleX = data.mixScaleX;
-			constraint.mixScaleY = data.mixScaleY;
-			constraint.mixShearY = data.mixShearY;
-		}
-
-		let pathConstraints = this.pathConstraints;
-		for (let i = 0, n = pathConstraints.length; i < n; i++) {
-			let constraint = pathConstraints[i];
-			let data = constraint.data;
-			constraint.position = data.position;
-			constraint.spacing = data.spacing;
-			constraint.mixRotate = data.mixRotate;
-			constraint.mixX = data.mixX;
-			constraint.mixY = data.mixY;
-		}
+		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();
 	}
 
 	/** Sets the slots and draw order to their setup pose values. */
@@ -560,12 +570,7 @@ export class Skeleton {
 	 * @return May be null. */
 	findIkConstraint (constraintName: string) {
 		if (!constraintName) throw new Error("constraintName cannot be null.");
-		let ikConstraints = this.ikConstraints;
-		for (let i = 0, n = ikConstraints.length; i < n; i++) {
-			let ikConstraint = ikConstraints[i];
-			if (ikConstraint.data.name == constraintName) return ikConstraint;
-		}
-		return 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
@@ -573,12 +578,7 @@ export class Skeleton {
 	 * @return May be null. */
 	findTransformConstraint (constraintName: string) {
 		if (!constraintName) throw new Error("constraintName cannot be null.");
-		let transformConstraints = this.transformConstraints;
-		for (let i = 0, n = transformConstraints.length; i < n; i++) {
-			let constraint = transformConstraints[i];
-			if (constraint.data.name == constraintName) return constraint;
-		}
-		return 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
@@ -586,12 +586,14 @@ export class Skeleton {
 	 * @return May be null. */
 	findPathConstraint (constraintName: string) {
 		if (!constraintName) throw new Error("constraintName cannot be null.");
-		let pathConstraints = this.pathConstraints;
-		for (let i = 0, n = pathConstraints.length; i < n; i++) {
-			let constraint = pathConstraints[i];
-			if (constraint.data.name == constraintName) return constraint;
-		}
-		return 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) {
+		if (constraintName == null) throw new Error("constraintName cannot be null.");
+		return this.physicsConstraints.find((constraint) => constraint.data.name == constraintName) ?? 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 }`.
@@ -641,4 +643,24 @@ export class Skeleton {
 		offset.set(minX, minY);
 		size.set(maxX - minX, maxY - minY);
 	}
+
+	/** Increments the skeleton's {@link #time}. */
+	update (delta: number) {
+		this.time += delta;
+	}
 }
+
+/** 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
+}

+ 23 - 6
spine-ts/spine-core/src/SkeletonData.ts

@@ -32,6 +32,7 @@ import { BoneData } from "./BoneData.js";
 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";
@@ -73,6 +74,9 @@ export class SkeletonData {
 	/** The skeleton's path constraints. */
 	pathConstraints = new Array<PathConstraintData>();
 
+	/** The skeleton's physics constraints. */
+	physicsConstraints = new Array<PhysicsConstraintData>();
+
 	/** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */
 	x: number = 0;
 
@@ -171,9 +175,9 @@ export class SkeletonData {
 	 * @return May be null. */
 	findIkConstraint (constraintName: string) {
 		if (!constraintName) throw new Error("constraintName cannot be null.");
-		let ikConstraints = this.ikConstraints;
+		const ikConstraints = this.ikConstraints;
 		for (let i = 0, n = ikConstraints.length; i < n; i++) {
-			let constraint = ikConstraints[i];
+			const constraint = ikConstraints[i];
 			if (constraint.name == constraintName) return constraint;
 		}
 		return null;
@@ -184,9 +188,9 @@ export class SkeletonData {
 	 * @return May be null. */
 	findTransformConstraint (constraintName: string) {
 		if (!constraintName) throw new Error("constraintName cannot be null.");
-		let transformConstraints = this.transformConstraints;
+		const transformConstraints = this.transformConstraints;
 		for (let i = 0, n = transformConstraints.length; i < n; i++) {
-			let constraint = transformConstraints[i];
+			const constraint = transformConstraints[i];
 			if (constraint.name == constraintName) return constraint;
 		}
 		return null;
@@ -197,9 +201,22 @@ export class SkeletonData {
 	 * @return May be null. */
 	findPathConstraint (constraintName: string) {
 		if (!constraintName) throw new Error("constraintName cannot be null.");
-		let pathConstraints = this.pathConstraints;
+		const pathConstraints = this.pathConstraints;
 		for (let i = 0, n = pathConstraints.length; i < n; i++) {
-			let constraint = pathConstraints[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;

+ 4 - 1
spine-ts/spine-core/src/Skin.ts

@@ -32,7 +32,7 @@ import { MeshAttachment } from "./attachments/MeshAttachment.js";
 import { BoneData } from "./BoneData.js";
 import { ConstraintData } from "./ConstraintData.js";
 import { Skeleton } from "./Skeleton.js";
-import { StringMap } from "./Utils.js";
+import { Color, StringMap } from "./Utils.js";
 
 /** Stores an entry in the skin consisting of the slot index, name, and attachment **/
 export class SkinEntry {
@@ -51,6 +51,9 @@ export class Skin {
 	bones = Array<BoneData>();
 	constraints = new Array<ConstraintData>();
 
+	/** 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
+
 	constructor (name: string) {
 		if (!name) throw new Error("name cannot be null.");
 		this.name = name;

+ 3 - 0
spine-ts/spine-core/src/SlotData.ts

@@ -55,6 +55,9 @@ export class SlotData {
 	/** The blend mode for drawing the slot's attachment. */
 	blendMode: BlendMode = BlendMode.Normal;
 
+	/** 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) {
 		if (index < 0) throw new Error("index must be >= 0.");
 		if (!name) throw new Error("name cannot be null.");

+ 14 - 4
spine-ts/spine-core/src/TransformConstraint.ts

@@ -28,7 +28,7 @@
  *****************************************************************************/
 
 import { Bone } from "./Bone.js";
-import { Skeleton } from "./Skeleton.js";
+import { Physics, Skeleton } from "./Skeleton.js";
 import { TransformConstraintData } from "./TransformConstraintData.js";
 import { Updatable } from "./Updatable.js";
 import { Vector2, MathUtils } from "./Utils.js";
@@ -79,7 +79,17 @@ export class TransformConstraint implements Updatable {
 		return this.active;
 	}
 
-	update () {
+	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;
 
 		if (this.data.local) {
@@ -240,7 +250,7 @@ export class TransformConstraint implements Updatable {
 			let rotation = bone.arotation;
 			if (mixRotate != 0) {
 				let r = target.arotation - rotation + this.data.offsetRotation;
-				r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360;
+				r -= Math.ceil(r / 360 - 0.5) * 360;
 				rotation += r * mixRotate;
 			}
 
@@ -257,7 +267,7 @@ export class TransformConstraint implements Updatable {
 			let shearY = bone.ashearY;
 			if (mixShearY != 0) {
 				let r = target.ashearY - shearY + this.data.offsetShearY;
-				r -= (16384 - ((16384.499999999996 - r / 360) | 0)) * 360;
+				r -= Math.ceil(r / 360 - 0.5) * 360;
 				shearY += r * mixShearY;
 			}
 

+ 10 - 4
spine-ts/spine-core/src/Updatable.ts

@@ -27,13 +27,19 @@
  * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
+import { Physics } from "./Skeleton.js";
+
 /** The interface for items updated by {@link Skeleton#updateWorldTransform()}. */
 export interface Updatable {
-	update (): void;
+	/** @param physics Determines how physics and other non-deterministic updates are applied. */
+	update (physics: Physics): void;
 
-	/** Returns false when this item has not been updated because a skin is required and the {@link Skeleton#skin active skin}
-	 * does not contain this item.
+	/** 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 Skin#getConstraints()
+	 * @see BoneData#getSkinRequired()
+	 * @see ConstraintData#getSkinRequired() */
 	isActive (): boolean;
 }

+ 5 - 0
spine-ts/spine-core/src/Utils.ts

@@ -179,6 +179,7 @@ export class Color {
 export class MathUtils {
 	static PI = 3.1415927;
 	static PI2 = MathUtils.PI * 2;
+	static invPI2 = 1 / MathUtils.PI2;
 	static radiansToDegrees = 180 / MathUtils.PI;
 	static radDeg = MathUtils.radiansToDegrees;
 	static degreesToRadians = MathUtils.PI / 180;
@@ -198,6 +199,10 @@ export class MathUtils {
 		return Math.sin(degrees * MathUtils.degRad);
 	}
 
+	static atan2Deg(y: number, x: number) {
+		return Math.atan2(y, x) * MathUtils.degRad;
+	}
+
 	static signum (value: number): number {
 		return value > 0 ? 1 : value < 0 ? -1 : 0;
 	}