From c7aac73dee99b2d30178b4783d8e9e80aa20b1c9 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 22 Nov 2023 16:55:41 +0100 Subject: [PATCH] [ts] Port of physics constraints, SkeletonJson and SkeletonBinary incomplete. --- spine-ts/spine-core/src/Animation.ts | 693 ++++++++++-------- spine-ts/spine-core/src/AnimationState.ts | 69 +- spine-ts/spine-core/src/Bone.ts | 142 ++-- spine-ts/spine-core/src/BoneData.ts | 8 +- spine-ts/spine-core/src/IkConstraint.ts | 26 +- spine-ts/spine-core/src/PathConstraint.ts | 21 +- spine-ts/spine-core/src/PhysicsConstraint.ts | 270 +++++++ .../spine-core/src/PhysicsConstraintData.ts | 71 ++ spine-ts/spine-core/src/Skeleton.ts | 152 ++-- spine-ts/spine-core/src/SkeletonData.ts | 29 +- spine-ts/spine-core/src/Skin.ts | 5 +- spine-ts/spine-core/src/SlotData.ts | 3 + .../spine-core/src/TransformConstraint.ts | 18 +- spine-ts/spine-core/src/Updatable.ts | 14 +- spine-ts/spine-core/src/Utils.ts | 5 + 15 files changed, 1045 insertions(+), 481 deletions(-) create mode 100644 spine-ts/spine-core/src/PhysicsConstraint.ts create mode 100644 spine-ts/spine-core/src/PhysicsConstraintData.ts diff --git a/spine-ts/spine-core/src/Animation.ts b/spine-ts/spine-core/src/Animation.ts index 670dd5542..b4c494ab1 100644 --- a/spine-ts/spine-core/src/Animation.ts +++ b/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 | 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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, 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 frameCount, inclusive. */ + setFrame (frame: number, time: number) { + this.frames[frame] = time; + } + + /** Resets the physics constraint when frames > lastTime and <= time. */ + apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, 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; } diff --git a/spine-ts/spine-core/src/AnimationState.ts b/spine-ts/spine-core/src/AnimationState.ts index eea5a4076..20c7c1b20 100644 --- a/spine-ts/spine-core/src/AnimationState.ts +++ b/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 alphaAttachmentThreshold, attachment timelines are applied. + * Defaults to 0, so attachment timelines are always applied. */ + alphaAttachmentThreshold: number = 0; + + /** When the mix percentage ({@link #getMixTime()} / {@link #getMixDuration()}) is less than the + * mixDrawOrderThreshold, draw order timelines are applied while this animation is being mixed out. Defaults to + * 0, so draw order timelines are not applied while this animation is being mixed out. */ + mixDrawOrderThreshold: number = 0; /** 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. + *

+ * See {@link AnimationState#apply(Skeleton)}. */ + wasApplied () { + return this.nextTrackLast != -1; + } } export class EventQueue { diff --git a/spine-ts/spine-core/src/Bone.ts b/spine-ts/spine-core/src/Bone.ts index c357d1ebe..c6aa2f215 100644 --- a/spine-ts/spine-core/src/Bone.ts +++ b/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. *

- * 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; } } diff --git a/spine-ts/spine-core/src/BoneData.ts b/spine-ts/spine-core/src/BoneData.ts index bab432378..ac79827b0 100644 --- a/spine-ts/spine-core/src/BoneData.ts +++ b/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."); diff --git a/spine-ts/spine-core/src/IkConstraint.ts b/spine-ts/spine-core/src/IkConstraint.ts index c320d6171..de5045a63 100644 --- a/spine-ts/spine-core/src/IkConstraint.ts +++ b/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, diff --git a/spine-ts/spine-core/src/PathConstraint.ts b/spine-ts/spine-core/src/PathConstraint.ts index 1b9247b20..8ec8e58f0 100644 --- a/spine-ts/spine-core/src/PathConstraint.ts +++ b/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); diff --git a/spine-ts/spine-core/src/PhysicsConstraint.ts b/spine-ts/spine-core/src/PhysicsConstraint.ts new file mode 100644 index 000000000..2f6e1d84f --- /dev/null +++ b/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. + *

+ * See Physics constraints 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(); + } +} diff --git a/spine-ts/spine-core/src/PhysicsConstraintData.ts b/spine-ts/spine-core/src/PhysicsConstraintData.ts new file mode 100644 index 000000000..c023388a4 --- /dev/null +++ b/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}. + *

+ * See Physics constraints 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); + } +} diff --git a/spine-ts/spine-core/src/Skeleton.ts b/spine-ts/spine-core/src/Skeleton.ts index 84cc8e030..c276e0600 100644 --- a/spine-ts/spine-core/src/Skeleton.ts +++ b/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; + + /** The skeleton's physics constraints. */ + physicsConstraints: Array; + /** The list of bones and constraints, sorted in the order they should be updated, as computed by {@link #updateCache()}. */ _updateCache = new Array(); @@ -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}. + *

+ * 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(); + 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 +} \ No newline at end of file diff --git a/spine-ts/spine-core/src/SkeletonData.ts b/spine-ts/spine-core/src/SkeletonData.ts index 0b5de49ef..479852bda 100644 --- a/spine-ts/spine-core/src/SkeletonData.ts +++ b/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(); + /** The skeleton's physics constraints. */ + physicsConstraints = new Array(); + /** 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; diff --git a/spine-ts/spine-core/src/Skin.ts b/spine-ts/spine-core/src/Skin.ts index ae5998044..a8de81d63 100644 --- a/spine-ts/spine-core/src/Skin.ts +++ b/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(); 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 + constructor (name: string) { if (!name) throw new Error("name cannot be null."); this.name = name; diff --git a/spine-ts/spine-core/src/SlotData.ts b/spine-ts/spine-core/src/SlotData.ts index 33388fee0..cd563ed9a 100644 --- a/spine-ts/spine-core/src/SlotData.ts +++ b/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."); diff --git a/spine-ts/spine-core/src/TransformConstraint.ts b/spine-ts/spine-core/src/TransformConstraint.ts index 13f621f53..649b85fe7 100644 --- a/spine-ts/spine-core/src/TransformConstraint.ts +++ b/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; } diff --git a/spine-ts/spine-core/src/Updatable.ts b/spine-ts/spine-core/src/Updatable.ts index 7143c88ae..e31de8786 100644 --- a/spine-ts/spine-core/src/Updatable.ts +++ b/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; } diff --git a/spine-ts/spine-core/src/Utils.ts b/spine-ts/spine-core/src/Utils.ts index ac9c2758d..e45315143 100644 --- a/spine-ts/spine-core/src/Utils.ts +++ b/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; }