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

This commit is contained in:
Mario Zechner 2023-11-22 16:55:41 +01:00
parent 5d9b361b1c
commit c7aac73dee
15 changed files with 1045 additions and 481 deletions

View File

@ -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;
}

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -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.");

View File

@ -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,

View File

@ -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);

View File

@ -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();
}
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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;

View File

@ -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;

View File

@ -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.");

View File

@ -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;
}

View File

@ -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;
}

View File

@ -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;
}