diff --git a/spine-ts/spine-core/src/IkConstraintData.ts b/spine-ts/spine-core/src/IkConstraintData.ts index 022dcb807..ec27303a0 100644 --- a/spine-ts/spine-core/src/IkConstraintData.ts +++ b/spine-ts/spine-core/src/IkConstraintData.ts @@ -47,7 +47,7 @@ export class IkConstraintData extends ConstraintData { } /** Controls the bend direction of the IK bones, either 1 or -1. */ - bendDirection = 1; + bendDirection = 0; /** When true and only a single bone is being constrained, if the target is too close, the bone is scaled to reach it. */ compress = false; @@ -61,7 +61,7 @@ export class IkConstraintData extends ConstraintData { uniform = false; /** A percentage (0-1) that controls the mix between the constrained and unconstrained rotations. */ - mix = 1; + mix = 0; /** For two bone IK, the distance from the maximum reach of the bones that rotation will slow. */ softness = 0; diff --git a/spine-ts/spine-core/src/PhysicsConstraint.ts b/spine-ts/spine-core/src/PhysicsConstraint.ts index db89eea74..3b5bf7731 100644 --- a/spine-ts/spine-core/src/PhysicsConstraint.ts +++ b/spine-ts/spine-core/src/PhysicsConstraint.ts @@ -134,7 +134,8 @@ export class PhysicsConstraint implements Updatable { this.reset(); // Fall through. case Physics.update: - this.remaining += Math.max(this.skeleton.time - this.lastTime, 0); + const delta = Math.max(this.skeleton.time - this.lastTime, 0); + this.remaining += delta; this.lastTime = this.skeleton.time; const bx = bone.worldX, by = bone.worldY; @@ -143,41 +144,52 @@ export class PhysicsConstraint implements Updatable { this.ux = bx; this.uy = by; } else { - let remaining = this.remaining, i = this.inertia, step = this.data.step; + let a = this.remaining, i = this.inertia, q = this.data.limit * delta, t = this.data.step, f = this.skeleton.data.referenceScale, d = -1; if (x || y) { if (x) { - this.xOffset += (this.ux - bx) * i; + const u = (this.ux - bx) * i; + this.xOffset += u > q ? q : u < -q ? -q : u; this.ux = bx; } if (y) { - this.yOffset += (this.uy - by) * i; + const u = (this.uy - by) * i; + this.yOffset += u > q ? q : u < -q ? -q : u; 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); + if (a >= t) { + d = Math.pow(this.damping, 60 * t); + const m = this.massInverse * t, e = this.strength, w = this.wind * f, g = this.gravity * f; do { if (x) { this.xVelocity += (w - this.xOffset * e) * m; - this.xOffset += this.xVelocity * step; + this.xOffset += this.xVelocity * t; this.xVelocity *= d; } if (y) { - this.yVelocity += (g - this.yOffset * e) * m; - this.yOffset += this.yVelocity * step; + this.yVelocity -= (g + this.yOffset * e) * m; + this.yOffset += this.yVelocity * t; this.yVelocity *= d; } - remaining -= step; - } while (remaining >= step); + a -= t; + } while (a >= t); } 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; + let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY; + if (dx > q) + dx = q; + else if (dx < -q) // + dx = -q; + if (dy > q) + dy = q; + else if (dy < -q) // + dy = -q; if (rotateOrShearX) { mr = (this.data.rotate + this.data.shearX) * mix; - let dx = this.cx - bone.worldX, dy = this.cy - bone.worldY, r = Math.atan2(dy + this.ty, dx + this.tx) - ca - this.rotateOffset * mr; + let 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); @@ -190,33 +202,33 @@ export class PhysicsConstraint implements Updatable { 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; + if (r > 0) this.scaleOffset += (dx * c + dy * 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); + a = this.remaining; + if (a >= t) { + if (d == -1) d = Math.pow(this.damping, 60 * t); + const m = this.massInverse * t, e = this.strength, w = this.wind, g = this.gravity, h = l / f; while (true) { - remaining -= step; + a -= t; if (scaleX) { this.scaleVelocity += (w * c - g * s - this.scaleOffset * e) * m; - this.scaleOffset += this.scaleVelocity * step; + this.scaleOffset += this.scaleVelocity * t; 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 -= ((w * s + g * c) * h + this.rotateOffset * e) * m; + this.rotateOffset += this.rotateVelocity * t; this.rotateVelocity *= d; - if (remaining < step) break; + if (a < t) break; const r = this.rotateOffset * mr + ca; c = Math.cos(r); s = Math.sin(r); - } else if (remaining < step) // + } else if (a < t) // break; } } } - this.remaining = remaining; + this.remaining = a; } this.cx = bone.worldX; this.cy = bone.worldY; @@ -268,6 +280,8 @@ export class PhysicsConstraint implements Updatable { bone.updateAppliedTransform(); } + /** Translates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone moved an additional + * amount in world space. */ translate (x: number, y: number) { this.ux -= x; this.uy -= y; @@ -278,10 +292,7 @@ export class PhysicsConstraint implements Updatable { /** Rotates the physics constraint so next {@link #update(Physics)} forces are applied as if the bone rotated around the * specified point in world space. */ rotate (x: number, y: number, degrees: number) { - let r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r); - r = this.tx * cos - this.ty * sin; - this.ty = this.tx * sin + this.ty * cos; - this.tx = r; + const r = degrees * MathUtils.degRad, cos = Math.cos(r), sin = Math.sin(r); const dx = this.cx - x, dy = this.cy - y; this.translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy); } diff --git a/spine-ts/spine-core/src/PhysicsConstraintData.ts b/spine-ts/spine-core/src/PhysicsConstraintData.ts index ec3a80b7f..3a646f62d 100644 --- a/spine-ts/spine-core/src/PhysicsConstraintData.ts +++ b/spine-ts/spine-core/src/PhysicsConstraintData.ts @@ -48,6 +48,7 @@ export class PhysicsConstraintData extends ConstraintData { rotate = 0; scaleX = 0; shearX = 0; + limit = 0; step = 0; inertia = 0; strength = 0; diff --git a/spine-ts/spine-core/src/Skeleton.ts b/spine-ts/spine-core/src/Skeleton.ts index ec52de6b7..0e12bbd55 100644 --- a/spine-ts/spine-core/src/Skeleton.ts +++ b/spine-ts/spine-core/src/Skeleton.ts @@ -54,7 +54,7 @@ export class Skeleton { /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ bones: Array; - /** The skeleton's slots. */ + /** The skeleton's slots in the setup pose draw order. */ slots: Array; /** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */ diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts index f1eef3c43..0f5271e04 100644 --- a/spine-ts/spine-core/src/SkeletonBinary.ts +++ b/spine-ts/spine-core/src/SkeletonBinary.ts @@ -80,11 +80,11 @@ export class SkeletonBinary { skeletonData.y = input.readFloat(); skeletonData.width = input.readFloat(); skeletonData.height = input.readFloat(); + skeletonData.referenceScale = input.readFloat() * scale; let nonessential = input.readBoolean(); if (nonessential) { skeletonData.fps = input.readFloat(); - skeletonData.imagesPath = input.readString(); skeletonData.audioPath = input.readString(); } @@ -128,6 +128,14 @@ export class SkeletonBinary { for (let i = 0; i < n; i++) { let slotName = input.readString(); if (!slotName) throw new Error("Slot name must not be null."); + let path: string | null = null; + if (nonessential) { + const slash = slotName!.lastIndexOf('/'); + if (slash != -1) { + path = slotName.substring(0, slash); + slotName = slotName.substring(slash + 1); + } + } let boneData = skeletonData.bones[input.readInt(true)]; let data = new SlotData(i, slotName, boneData); Color.rgba8888ToColor(data.color, input.readInt32()); @@ -137,7 +145,10 @@ export class SkeletonBinary { data.attachmentName = input.readStringRef(); data.blendMode = input.readInt(true); - if (nonessential) data.visible = input.readBoolean(); + if (nonessential) { + data.visible = input.readBoolean(); + data.path = path; + } skeletonData.slots.push(data); } @@ -152,14 +163,14 @@ export class SkeletonBinary { for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); data.target = skeletonData.bones[input.readInt(true)]; - data.mix = input.readFloat(); - data.softness = input.readFloat() * scale; let flags = input.readByte(); data.skinRequired = (flags & 1) != 0; data.bendDirection = (flags & 2) != 0 ? 1 : -1; data.compress = (flags & 4) != 0; data.stretch = (flags & 8) != 0; data.uniform = (flags & 16) != 0; + if ((flags & 32) != 0) data.mix = (flags & 64) != 0 ? input.readFloat() : 1; + if ((flags & 128) != 0) data.softness = input.readFloat() * scale; skeletonData.ikConstraints.push(data); } @@ -174,22 +185,23 @@ export class SkeletonBinary { for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); data.target = skeletonData.bones[input.readInt(true)]; - const flags = input.readByte(); + let flags = input.readByte(); data.skinRequired = (flags & 1) != 0; data.local = (flags & 2) != 0; data.relative = (flags & 4) != 0; - data.offsetRotation = input.readFloat(); - data.offsetX = input.readFloat() * scale; - data.offsetY = input.readFloat() * scale; - data.offsetScaleX = input.readFloat(); - data.offsetScaleY = input.readFloat(); - data.offsetShearY = input.readFloat(); - data.mixRotate = input.readFloat(); - data.mixX = input.readFloat(); - data.mixY = input.readFloat(); - data.mixScaleX = input.readFloat(); - data.mixScaleY = input.readFloat(); - data.mixShearY = input.readFloat(); + if ((flags & 8) != 0) data.offsetRotation = input.readFloat(); + if ((flags & 16) != 0) data.offsetX = input.readFloat() * scale; + if ((flags & 32) != 0) data.offsetY = input.readFloat() * scale; + if ((flags & 64) != 0) data.offsetScaleX = input.readFloat(); + if ((flags & 128) != 0) data.offsetScaleY = input.readFloat(); + flags = input.readByte(); + if ((flags & 1) != 0) data.offsetShearY = input.readFloat(); + if ((flags & 2) != 0) data.mixRotate = input.readFloat(); + if ((flags & 4) != 0) data.mixX = input.readFloat(); + if ((flags & 8) != 0) data.mixY = input.readFloat(); + if ((flags & 16) != 0) data.mixScaleX = input.readFloat(); + if ((flags & 32) != 0) data.mixScaleY = input.readFloat(); + if ((flags & 64) != 0) data.mixShearY = input.readFloat(); skeletonData.transformConstraints.push(data); } @@ -205,10 +217,11 @@ export class SkeletonBinary { for (let ii = 0; ii < nn; ii++) data.bones.push(skeletonData.bones[input.readInt(true)]); data.target = skeletonData.slots[input.readInt(true)]; - data.positionMode = input.readInt(true); - data.spacingMode = input.readInt(true); - data.rotateMode = input.readInt(true); - data.offsetRotation = input.readFloat(); + const flags = input.readByte(); + data.positionMode = flags & 1; + data.spacingMode = (flags >> 1) & 3; + data.rotateMode = (flags >> 3) & 3; + if ((flags & 128) != 0) data.offsetRotation = input.readFloat(); data.position = input.readFloat(); if (data.positionMode == PositionMode.Fixed) data.position *= scale; data.spacing = input.readFloat(); @@ -234,14 +247,14 @@ export class SkeletonBinary { if ((flags & 8) != 0) data.rotate = input.readFloat(); if ((flags & 16) != 0) data.scaleX = input.readFloat(); if ((flags & 32) != 0) data.shearX = input.readFloat(); + data.limit = ((flags & 64) != 0 ? input.readFloat() : 5000) * scale; data.step = 1 / input.readByte(); data.inertia = input.readFloat(); data.strength = input.readFloat(); data.damping = input.readFloat(); - data.massInverse = input.readFloat(); - data.wind = input.readFloat() * scale; - data.gravity = input.readFloat() * scale; - data.mix = input.readFloat(); + data.massInverse = (flags & 128) != 0 ? input.readFloat() : 1; + data.wind = input.readFloat(); + data.gravity = input.readFloat(); flags = input.readByte(); if ((flags & 1) != 0) data.inertiaGlobal = true; if ((flags & 2) != 0) data.strengthGlobal = true; @@ -250,6 +263,7 @@ export class SkeletonBinary { if ((flags & 16) != 0) data.windGlobal = true; if ((flags & 32) != 0) data.gravityGlobal = true; if ((flags & 64) != 0) data.mixGlobal = true; + data.mix = (flags & 128) != 0 ? input.readFloat() : 1; skeletonData.physicsConstraints.push(data); } @@ -365,7 +379,7 @@ export class SkeletonBinary { let path = (flags & 16) != 0 ? input.readStringRef() : null; const color = (flags & 32) != 0 ? input.readInt32() : 0xffffffff; const sequence = (flags & 64) != 0 ? this.readSequence(input) : null; - let rotation = input.readFloat(); + let rotation = (flags & 128) != 0 ? input.readFloat() : 0; let x = input.readFloat(); let y = input.readFloat(); let scaleX = input.readFloat(); @@ -827,19 +841,20 @@ export class SkeletonBinary { for (let i = 0, n = input.readInt(true); i < n; i++) { let index = input.readInt(true), frameCount = input.readInt(true), frameLast = frameCount - 1; let timeline = new IkConstraintTimeline(frameCount, input.readInt(true), index); - let time = input.readFloat(), mix = input.readFloat(), softness = input.readFloat() * scale; + let flags = input.readByte(); + let time = input.readFloat(), mix = (flags & 1) != 0 ? ((flags & 2) != 0 ? input.readFloat() : 1) : 0; + let softness = (flags & 4) != 0 ? input.readFloat() * scale : 0; for (let frame = 0, bezier = 0; ; frame++) { - const flags = input.readByte(); - timeline.setFrame(frame, time, mix, softness, input.readByte(), (flags & 1) != 0, (flags & 2) != 0); + timeline.setFrame(frame, time, mix, softness, (flags & 8) != 0 ? 1 : -1, (flags & 16) != 0, (flags & 32) != 0); if (frame == frameLast) break; - let time2 = input.readFloat(), mix2 = input.readFloat(), softness2 = input.readFloat() * scale; - switch (input.readByte()) { - case CURVE_STEPPED: - timeline.setStepped(frame); - break; - case CURVE_BEZIER: - setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1); - setBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale); + flags = input.readByte(); + const time2 = input.readFloat(), mix2 = (flags & 1) != 0 ? ((flags & 2) != 0 ? input.readFloat() : 1) : 0; + const softness2 = (flags & 4) != 0 ? input.readFloat() * scale : 0; + if ((flags & 64) != 0) { + timeline.setStepped(frame); + } else if ((flags & 128) != 0) { + setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1); + setBezier(input, timeline, bezier++, frame, 1, time, time2, softness, softness2, scale); } time = time2; mix = mix2; @@ -953,10 +968,10 @@ export class SkeletonBinary { timelines.push(readTimeline1(input, new PhysicsConstraintMassTimeline(frameCount, bezierCount, index), 1)); break; case PHYSICS_WIND: - timelines.push(readTimeline1(input, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), scale)); + timelines.push(readTimeline1(input, new PhysicsConstraintWindTimeline(frameCount, bezierCount, index), 1)); break; case PHYSICS_GRAVITY: - timelines.push(readTimeline1(input, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), scale)); + timelines.push(readTimeline1(input, new PhysicsConstraintGravityTimeline(frameCount, bezierCount, index), 1)); break; case PHYSICS_MIX: timelines.push(readTimeline1(input, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1)); diff --git a/spine-ts/spine-core/src/SkeletonData.ts b/spine-ts/spine-core/src/SkeletonData.ts index 479852bda..49cfee59f 100644 --- a/spine-ts/spine-core/src/SkeletonData.ts +++ b/spine-ts/spine-core/src/SkeletonData.ts @@ -49,8 +49,9 @@ export class SkeletonData { /** The skeleton's bones, sorted parent first. The root bone is always the first bone. */ bones = new Array(); // Ordered parents first. - /** The skeleton's slots. */ + /** The skeleton's slots in the setup pose draw order. */ slots = new Array(); // Setup pose draw order. + skins = new Array(); /** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine. @@ -89,6 +90,10 @@ export class SkeletonData { /** The height of the skeleton's axis aligned bounding box in the setup pose. */ height: number = 0; + /** Baseline scale factor for applying distance-dependent effects on non-scalable properties, such as angle or scale. Default + * is 100. */ + referenceScale = 100; + /** The Spine version used to export the skeleton data, or null. */ version: string | null = null; diff --git a/spine-ts/spine-core/src/SkeletonJson.ts b/spine-ts/spine-core/src/SkeletonJson.ts index 03811191f..f422c3820 100644 --- a/spine-ts/spine-core/src/SkeletonJson.ts +++ b/spine-ts/spine-core/src/SkeletonJson.ts @@ -79,8 +79,10 @@ export class SkeletonJson { skeletonData.y = skeletonMap.y; skeletonData.width = skeletonMap.width; skeletonData.height = skeletonMap.height; + skeletonData.referenceScale = getValue(skeletonMap, "referenceScale", 100) * scale; skeletonData.fps = skeletonMap.fps; - skeletonData.imagesPath = skeletonMap.images; + skeletonData.imagesPath = skeletonMap.images ?? null; + skeletonData.audioPath = skeletonMap.audio ?? null; } // Bones @@ -114,9 +116,16 @@ export class SkeletonJson { if (root.slots) { for (let i = 0; i < root.slots.length; i++) { let slotMap = root.slots[i]; + let path: string | null = null; + let slotName = slotMap.name; + const slash = slotName.lastIndexOf('/'); + if (slash != -1) { + path = slotName.substring(0, slash); + slotName = slotName.substring(slash + 1); + } let boneData = skeletonData.findBone(slotMap.bone); - if (!boneData) throw new Error(`Couldn't find bone ${slotMap.bone} for slot ${slotMap.name}`); - let data = new SlotData(skeletonData.slots.length, slotMap.name, boneData); + if (!boneData) throw new Error(`Couldn't find bone ${slotMap.bone} for slot ${slotName}`); + let data = new SlotData(skeletonData.slots.length, slotName, boneData); let color: string = getValue(slotMap, "color", null); if (color) data.color.setFromString(color); @@ -126,6 +135,8 @@ export class SkeletonJson { data.attachmentName = getValue(slotMap, "attachment", null); data.blendMode = Utils.enumValue(BlendMode, getValue(slotMap, "blend", "normal")); + data.visible = getValue(slotMap, "visible", true); + data.path = path; skeletonData.slots.push(data); } } @@ -253,13 +264,14 @@ export class SkeletonJson { data.rotate = getValue(constraintMap, "rotate", 0); data.scaleX = getValue(constraintMap, "scaleX", 0); data.shearX = getValue(constraintMap, "shearX", 0); + data.limit = getValue(constraintMap, "limit", 5000) * scale; data.step = 1 / getValue(constraintMap, "fps", 60); data.inertia = getValue(constraintMap, "inertia", 1); data.strength = getValue(constraintMap, "strength", 100); data.damping = getValue(constraintMap, "damping", 1); data.massInverse = 1 / getValue(constraintMap, "mass", 1); - data.wind = getValue(constraintMap, "wind", 0) * scale; - data.gravity = getValue(constraintMap, "gravity", 0) * scale; + data.wind = getValue(constraintMap, "wind", 0); + data.gravity = getValue(constraintMap, "gravity", 0); data.mix = getValue(constraintMap, "mix", 1); data.inertiaGlobal = getValue(constraintMap, "inertiaGlobal", false); data.strengthGlobal = getValue(constraintMap, "strengthGlobal", false); @@ -911,7 +923,6 @@ export class SkeletonJson { } let timeline; - let timelineScale = 1; if (timelineName == "inertia") timeline = new PhysicsConstraintInertiaTimeline(frames, frames, constraintIndex); else if (timelineName == "strength") @@ -920,19 +931,15 @@ export class SkeletonJson { timeline = new PhysicsConstraintDampingTimeline(frames, frames, constraintIndex); else if (timelineName == "mass") timeline = new PhysicsConstraintMassTimeline(frames, frames, constraintIndex); - else if (timelineName == "wind") { + else if (timelineName == "wind") timeline = new PhysicsConstraintWindTimeline(frames, frames, constraintIndex); - timelineScale = scale; - } - else if (timelineName == "gravity") { + else if (timelineName == "gravity") timeline = new PhysicsConstraintGravityTimeline(frames, frames, constraintIndex); - timelineScale = scale; - } else if (timelineName == "mix") // timeline = new PhysicsConstraintMixTimeline(frames, frames, constraintIndex); else continue; - timelines.push(readTimeline1(timelineMap, timeline, 0, timelineScale)); + timelines.push(readTimeline1(timelineMap, timeline, 0, 1)); } } } diff --git a/spine-ts/spine-core/src/SlotData.ts b/spine-ts/spine-core/src/SlotData.ts index cd563ed9a..079dff047 100644 --- a/spine-ts/spine-core/src/SlotData.ts +++ b/spine-ts/spine-core/src/SlotData.ts @@ -58,6 +58,10 @@ export class SlotData { /** False if the slot was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */ visible = true; + /** The folders for this slot in the draw order, delimited by /, or null if nonessential data was not exported. */ + path: string | null = null; + + 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.");