diff --git a/spine-ts/spine-core/src/Skeleton.ts b/spine-ts/spine-core/src/Skeleton.ts index 1c730cd7f..6322d2688 100644 --- a/spine-ts/spine-core/src/Skeleton.ts +++ b/spine-ts/spine-core/src/Skeleton.ts @@ -299,7 +299,7 @@ export class Skeleton { let constrained = constraint.bones; let boneCount = constrained.length; - if (constraint.data.local) { + if (constraint.data.localFrom) { for (let i = 0; i < boneCount; i++) { let child = constrained[i]; this.sortBone(child.parent!); diff --git a/spine-ts/spine-core/src/SkeletonBinary.ts b/spine-ts/spine-core/src/SkeletonBinary.ts index 518d1ec51..555db38db 100644 --- a/spine-ts/spine-core/src/SkeletonBinary.ts +++ b/spine-ts/spine-core/src/SkeletonBinary.ts @@ -42,7 +42,7 @@ import { PhysicsConstraintData } from "./PhysicsConstraintData.js"; import { SkeletonData } from "./SkeletonData.js"; import { Skin } from "./Skin.js"; import { SlotData } from "./SlotData.js"; -import { TransformConstraintData } from "./TransformConstraintData.js"; +import { FromProperty, FromRotate, FromScaleX, FromScaleY, FromShearY, FromX, FromY, ToProperty, ToRotate, ToScaleX, ToScaleY, ToShearY, ToX, ToY, TransformConstraintData } from "./TransformConstraintData.js"; import { Color, Utils } from "./Utils.js"; /** Loads skeleton data in the Spine binary format. @@ -174,23 +174,57 @@ 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)]; - let flags = input.readByte(); + let flags = input.readUnsignedByte(); data.skinRequired = (flags & 1) != 0; - data.local = (flags & 2) != 0; - data.relative = (flags & 4) != 0; - 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(); + data.localFrom = (flags & 2) != 0; + data.localTo = (flags & 4) != 0; + data.relative = (flags & 8) != 0; + data.clamp = (flags & 16) != 0; + + nn = flags >> 5; + for (let ii = 0, tn; ii < nn; ii++) { + let from: FromProperty | null; + let type = input.readByte(); + switch (type) { + case 0: from = new FromRotate(); break; + case 1: from = new FromX(); break; + case 2: from = new FromY(); break; + case 3: from = new FromScaleX(); break; + case 4: from = new FromScaleY(); break; + case 5: from = new FromShearY(); break; + default: from = null; + } + if (!from) continue; + from.offset = input.readFloat() * scale; + tn = input.readByte(); + for (let t = 0; t < tn; t++) { + let to: ToProperty | null; + type = input.readByte(); + switch (type) { + case 0: to = new ToRotate(); break; + case 1: to = new ToX(); break; + case 2: to = new ToY(); break; + case 3: to = new ToScaleX(); break; + case 4: to = new ToScaleY(); break; + case 5: to = new ToShearY(); break; + default: to = null; + } + if (!to) continue; + to.offset = input.readFloat() * scale; + to.max = input.readFloat() * scale; + to.scale = input.readFloat(); + from.to[t] = to; + } + data.properties[ii] = from; + } + 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(); + if ((flags & 1) != 0) data.mixRotate = input.readFloat(); + if ((flags & 2) != 0) data.mixX = input.readFloat(); + if ((flags & 4) != 0) data.mixY = input.readFloat(); + if ((flags & 8) != 0) data.mixScaleX = input.readFloat(); + if ((flags & 16) != 0) data.mixScaleY = input.readFloat(); + if ((flags & 32) != 0) data.mixShearY = input.readFloat(); skeletonData.transformConstraints.push(data); } @@ -972,6 +1006,8 @@ export class SkeletonBinary { break; case PHYSICS_MIX: timelines.push(readTimeline1(input, new PhysicsConstraintMixTimeline(frameCount, bezierCount, index), 1)); + default: + throw new Error("Unknown physics timeline type."); } } } diff --git a/spine-ts/spine-core/src/SkeletonJson.ts b/spine-ts/spine-core/src/SkeletonJson.ts index b9aaf1d32..6a65deffc 100644 --- a/spine-ts/spine-core/src/SkeletonJson.ts +++ b/spine-ts/spine-core/src/SkeletonJson.ts @@ -39,7 +39,7 @@ import { PathConstraintData, PositionMode, SpacingMode, RotateMode } from "./Pat import { SkeletonData } from "./SkeletonData.js"; import { Skin } from "./Skin.js"; import { SlotData, BlendMode } from "./SlotData.js"; -import { TransformConstraintData } from "./TransformConstraintData.js"; +import { FromProperty, FromRotate, FromScaleX, FromScaleY, FromShearY, FromX, FromY, ToProperty, ToRotate, ToScaleX, ToScaleY, ToShearY, ToX, ToY, TransformConstraintData } from "./TransformConstraintData.js"; import { Utils, Color, NumberArrayLike } from "./Utils.js"; import { Sequence, SequenceMode } from "./attachments/Sequence.js"; import { SequenceTimeline } from "./Animation.js"; @@ -184,14 +184,47 @@ export class SkeletonJson { if (!target) throw new Error(`Couldn't find target bone ${targetName} for transform constraint ${constraintMap.name}.`); data.target = target; - data.local = getValue(constraintMap, "local", false); + data.localFrom = getValue(constraintMap, "localFrom", false); + data.localFrom = getValue(constraintMap, "localTo", false); data.relative = getValue(constraintMap, "relative", false); - data.offsetRotation = getValue(constraintMap, "rotation", 0); - data.offsetX = getValue(constraintMap, "x", 0) * scale; - data.offsetY = getValue(constraintMap, "y", 0) * scale; - data.offsetScaleX = getValue(constraintMap, "scaleX", 0); - data.offsetScaleY = getValue(constraintMap, "scaleY", 0); - data.offsetShearY = getValue(constraintMap, "shearY", 0); + data.clamp = getValue(constraintMap, "clamp", false); + + const propertiesEntries = Object.entries(getValue(constraintMap, "properties", {})) as [string, any][]; + for (let ii = 0; ii < propertiesEntries.length; ii++) { + let name = propertiesEntries[ii][0]; + let from: FromProperty; + switch (name) { + case "rotate": from = new FromRotate(); break; + case "x": from = new FromX(); break; + case "y": from = new FromY(); break; + case "scaleX": from = new FromScaleX(); break; + case "scaleY": from = new FromScaleY(); break; + case "shearY": from = new FromShearY(); break; + default: throw new Error("Invalid transform constraint from property: " + name); + } + const fromEntry = propertiesEntries[ii][1]; + from.offset = getValue(fromEntry, "offset", 0) * scale; + const toEntries = Object.entries(getValue(fromEntry, "to", {})) as [string, any][]; + for (let t = 0; t < toEntries.length; t++) { + let name = toEntries[t][0]; + let to: ToProperty; + switch (name) { + case "rotate": to = new ToRotate(); break; + case "x": to = new ToX(); break; + case "y": to = new ToY(); break; + case "scaleX": to = new ToScaleX(); break; + case "scaleY": to = new ToScaleY(); break; + case "shearY": to = new ToShearY(); break; + default: throw new Error("Invalid transform constraint to property: " + name); + } + let toEntry = toEntries[t][1]; + to.offset = getValue(toEntry, "offset", 0) * scale; + to.max = getValue(toEntry, "max", 1) * scale; + to.scale = getValue(toEntry, "scale", 1); + from.to.push(to); + } + if (from.to.length > 0) data.properties.push(from); + } data.mixRotate = getValue(constraintMap, "mixRotate", 1); data.mixX = getValue(constraintMap, "mixX", 1); @@ -687,7 +720,10 @@ export class SkeletonJson { } timelines.push(timeline); + } else { + throw new Error("Invalid timeline type for a slot: " + timelineMap.name + " (" + slotMap.name + ")"); } + } } } @@ -740,6 +776,8 @@ export class SkeletonJson { timeline.setFrame(frame, getValue(aFrame, "time", 0), Utils.enumValue(Inherit, getValue(aFrame, "inherit", "Normal"))); } timelines.push(timeline); + } else { + throw new Error("Invalid timeline type for a bone: " + timelineMap.name + " (" + boneMap.name + ")"); } } } diff --git a/spine-ts/spine-core/src/TransformConstraint.ts b/spine-ts/spine-core/src/TransformConstraint.ts index bd786a89d..667370957 100644 --- a/spine-ts/spine-core/src/TransformConstraint.ts +++ b/spine-ts/spine-core/src/TransformConstraint.ts @@ -94,199 +94,36 @@ export class TransformConstraint implements Updatable { 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) { - if (this.data.relative) - this.applyRelativeLocal(); + const data = this.data, localFrom = data.localFrom, localTo = data.localTo, relative = data.relative, clamp = data.clamp; + const target = this.target; + const fromItems = data.properties; + const fn = data.properties.length; + const bones = this.bones; + for (let i = 0, n = this.bones.length; i < n; i++) { + const bone = bones[i]; + for (let f = 0; f < fn; f++) { + const from = fromItems[f]; + const mix = from.mix(this); + if (mix != 0) { + const value = from.value(target, localFrom) - from.offset; + const toItems = from.to; + for (let t = 0, tn = from.to.length; t < tn; t++) { + var to = toItems[t]; + let clamped = to.offset + value * to.scale; + if (clamp) { + if (to.offset < to.max) + clamped = MathUtils.clamp(clamped, to.offset, to.max); + else + clamped = MathUtils.clamp(clamped, to.max, to.offset); + } + to.apply(bone, clamped, localTo, relative, mix); + } + } + } + if (localTo) + bone.update(null); else - this.applyAbsoluteLocal(); - } else { - if (this.data.relative) - this.applyRelativeWorld(); - else - this.applyAbsoluteWorld(); - } - } - - applyAbsoluteWorld () { - let mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, - mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; - let translate = mixX != 0 || mixY != 0; - - let target = this.target; - let ta = target.a, tb = target.b, tc = target.c, td = target.d; - let degRadReflect = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad; - let offsetRotation = this.data.offsetRotation * degRadReflect; - let offsetShearY = this.data.offsetShearY * degRadReflect; - - let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - - if (mixRotate != 0) { - let a = bone.a, b = bone.b, c = bone.c, d = bone.d; - let r = Math.atan2(tc, ta) - Math.atan2(c, a) + offsetRotation; - if (r > MathUtils.PI) - r -= MathUtils.PI2; - else if (r < -MathUtils.PI) // - r += MathUtils.PI2; - r *= mixRotate; - let cos = Math.cos(r), sin = Math.sin(r); - bone.a = cos * a - sin * c; - bone.b = cos * b - sin * d; - bone.c = sin * a + cos * c; - bone.d = sin * b + cos * d; - } - - if (translate) { - let temp = this.temp; - target.localToWorld(temp.set(this.data.offsetX, this.data.offsetY)); - bone.worldX += (temp.x - bone.worldX) * mixX; - bone.worldY += (temp.y - bone.worldY) * mixY; - } - - if (mixScaleX != 0) { - let s = Math.sqrt(bone.a * bone.a + bone.c * bone.c); - if (s != 0) s = (s + (Math.sqrt(ta * ta + tc * tc) - s + this.data.offsetScaleX) * mixScaleX) / s; - bone.a *= s; - bone.c *= s; - } - if (mixScaleY != 0) { - let s = Math.sqrt(bone.b * bone.b + bone.d * bone.d); - if (s != 0) s = (s + (Math.sqrt(tb * tb + td * td) - s + this.data.offsetScaleY) * mixScaleY) / s; - bone.b *= s; - bone.d *= s; - } - - if (mixShearY > 0) { - let b = bone.b, d = bone.d; - let by = Math.atan2(d, b); - let r = Math.atan2(td, tb) - Math.atan2(tc, ta) - (by - Math.atan2(bone.c, bone.a)); - if (r > MathUtils.PI) - r -= MathUtils.PI2; - else if (r < -MathUtils.PI) // - r += MathUtils.PI2; - r = by + (r + offsetShearY) * mixShearY; - let s = Math.sqrt(b * b + d * d); - bone.b = Math.cos(r) * s; - bone.d = Math.sin(r) * s; - } - - bone.updateAppliedTransform(); - } - } - - applyRelativeWorld () { - let mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, - mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; - let translate = mixX != 0 || mixY != 0; - - let target = this.target; - let ta = target.a, tb = target.b, tc = target.c, td = target.d; - let degRadReflect = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad; - let offsetRotation = this.data.offsetRotation * degRadReflect, offsetShearY = this.data.offsetShearY * degRadReflect; - - let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - - if (mixRotate != 0) { - let a = bone.a, b = bone.b, c = bone.c, d = bone.d; - let r = Math.atan2(tc, ta) + offsetRotation; - if (r > MathUtils.PI) - r -= MathUtils.PI2; - else if (r < -MathUtils.PI) // - r += MathUtils.PI2; - r *= mixRotate; - let cos = Math.cos(r), sin = Math.sin(r); - bone.a = cos * a - sin * c; - bone.b = cos * b - sin * d; - bone.c = sin * a + cos * c; - bone.d = sin * b + cos * d; - } - - if (translate) { - let temp = this.temp; - target.localToWorld(temp.set(this.data.offsetX, this.data.offsetY)); - bone.worldX += temp.x * mixX; - bone.worldY += temp.y * mixY; - } - - if (mixScaleX != 0) { - let s = (Math.sqrt(ta * ta + tc * tc) - 1 + this.data.offsetScaleX) * mixScaleX + 1; - bone.a *= s; - bone.c *= s; - } - if (mixScaleY != 0) { - let s = (Math.sqrt(tb * tb + td * td) - 1 + this.data.offsetScaleY) * mixScaleY + 1; - bone.b *= s; - bone.d *= s; - } - - if (mixShearY > 0) { - let r = Math.atan2(td, tb) - Math.atan2(tc, ta); - if (r > MathUtils.PI) - r -= MathUtils.PI2; - else if (r < -MathUtils.PI) // - r += MathUtils.PI2; - let b = bone.b, d = bone.d; - r = Math.atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * mixShearY; - let s = Math.sqrt(b * b + d * d); - bone.b = Math.cos(r) * s; - bone.d = Math.sin(r) * s; - } - - bone.updateAppliedTransform(); - } - } - - applyAbsoluteLocal () { - let mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, - mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; - - let target = this.target; - - let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - - let rotation = bone.arotation; - if (mixRotate != 0) rotation += (target.arotation - rotation + this.data.offsetRotation) * mixRotate; - - let x = bone.ax, y = bone.ay; - x += (target.ax - x + this.data.offsetX) * mixX; - y += (target.ay - y + this.data.offsetY) * mixY; - - let scaleX = bone.ascaleX, scaleY = bone.ascaleY; - if (mixScaleX != 0 && scaleX != 0) - scaleX = (scaleX + (target.ascaleX - scaleX + this.data.offsetScaleX) * mixScaleX) / scaleX; - if (mixScaleY != 0 && scaleY != 0) - scaleY = (scaleY + (target.ascaleY - scaleY + this.data.offsetScaleY) * mixScaleY) / scaleY; - - let shearY = bone.ashearY; - if (mixShearY != 0) shearY += (target.ashearY - shearY + this.data.offsetShearY) * mixShearY; - - bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); - } - } - - applyRelativeLocal () { - let mixRotate = this.mixRotate, mixX = this.mixX, mixY = this.mixY, mixScaleX = this.mixScaleX, - mixScaleY = this.mixScaleY, mixShearY = this.mixShearY; - - let target = this.target; - - let bones = this.bones; - for (let i = 0, n = bones.length; i < n; i++) { - let bone = bones[i]; - - let rotation = bone.arotation + (target.arotation + this.data.offsetRotation) * mixRotate; - let x = bone.ax + (target.ax + this.data.offsetX) * mixX; - let y = bone.ay + (target.ay + this.data.offsetY) * mixY; - let scaleX = bone.ascaleX * (((target.ascaleX - 1 + this.data.offsetScaleX) * mixScaleX) + 1); - let scaleY = bone.ascaleY * (((target.ascaleY - 1 + this.data.offsetScaleY) * mixScaleY) + 1); - let shearY = bone.ashearY + (target.ashearY + this.data.offsetShearY) * mixShearY; - - bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY); + bone.updateAppliedTransform(); } } } diff --git a/spine-ts/spine-core/src/TransformConstraintData.ts b/spine-ts/spine-core/src/TransformConstraintData.ts index a217cbd07..89f162d3e 100644 --- a/spine-ts/spine-core/src/TransformConstraintData.ts +++ b/spine-ts/spine-core/src/TransformConstraintData.ts @@ -29,6 +29,9 @@ import { ConstraintData } from "./ConstraintData.js"; import { BoneData } from "./BoneData.js"; +import { Bone } from "./Bone.js"; +import { TransformConstraint } from "./TransformConstraint.js"; +import { MathUtils } from "./Utils.js"; /** Stores the setup pose for a {@link TransformConstraint}. * @@ -53,28 +56,227 @@ export class TransformConstraintData extends ConstraintData { mixScaleY = 0; mixShearY = 0; - /** An offset added to the constrained bone rotation. */ - offsetRotation = 0; + /** Reads the target bone's local transform instead of its world transform. */ + localFrom = false; - /** An offset added to the constrained bone X translation. */ - offsetX = 0; - - /** An offset added to the constrained bone Y translation. */ - offsetY = 0; - - /** An offset added to the constrained bone scaleX. */ - offsetScaleX = 0; - - /** An offset added to the constrained bone scaleY. */ - offsetScaleY = 0; - - /** An offset added to the constrained bone shearY. */ - offsetShearY = 0; + /** Sets the constrained bones' local transforms instead of their world transforms. */ + localTo = false; + /** Adds the target bone transform to the constrained bones instead of setting it absolutely. */ relative = false; - local = false; + + /** Prevents constrained bones from exceeding the ranged defined by {@link ToProperty#offset} and {@link ToProperty#max}. */ + clamp = false; + + /** The mapping of transform properties to other transform properties. */ + readonly properties: Array = []; constructor (name: string) { super(name, 0, false); } + } + +/** Source property for a {@link TransformConstraint}. */ +export abstract class FromProperty { + /** The value of this property that corresponds to {@link ToProperty#offset}. */ + offset = 0; + + /** Constrained properties. */ + readonly to: Array = []; + + /** Reads this property from the specified bone. */ + abstract value (target: Bone, local: boolean): number; + + /** Reads the mix for this property from the specified constraint. */ + abstract mix (constraint: TransformConstraint): number; +} + +/** Constrained property for a {@link TransformConstraint}. */ +export abstract class ToProperty { + /** The value of this property that corresponds to {@link FromProperty#offset}. */ + offset = 0; + + /** The maximum value of this property when {@link TransformConstraintData#clamp clamped}. */ + max = 0; + + /** The scale of the {@link FromProperty} value in relation to this property. */ + scale = 0; + + /** Applies the value to this property. */ + abstract apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void; +} + +export class FromRotate extends FromProperty { + value (target: Bone, local: boolean): number { + return local ? target.arotation : Math.atan2(target.c, target.a) * MathUtils.radDeg; + } + + mix (constraint: TransformConstraint): number { + return constraint.mixRotate; + } +} + +export class ToRotate extends ToProperty { + apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void { + if (local) { + if (!relative) value -= bone.arotation; + bone.arotation += value * mix; + } else { + const a = bone.a, b = bone.b, c = bone.c, d = bone.d; + value *= MathUtils.degRad; + if (!relative) value -= Math.atan2(c, a); + if (value > MathUtils.PI) + value -= MathUtils.PI2; + else if (value < -MathUtils.PI) // + value += MathUtils.PI2; + value *= mix; + const cos = Math.cos(value), sin = Math.sin(value); + bone.a = cos * a - sin * c; + bone.b = cos * b - sin * d; + bone.c = sin * a + cos * c; + bone.d = sin * b + cos * d; + } + } +} + +export class FromX extends FromProperty { + value (target: Bone, local: boolean): number { + return local ? target.ax : target.worldX; + } + + mix (constraint: TransformConstraint): number { + return constraint.mixX; + } +} + +export class ToX extends ToProperty { + apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void { + if (local) { + if (!relative) value -= bone.ax; + bone.ax += value * mix; + } else { + if (!relative) value -= bone.worldX; + bone.worldX += value * mix; + } + } +} + +export class FromY extends FromProperty { + value (target: Bone, local: boolean): number { + return local ? target.ay : target.worldY; + } + + mix (constraint: TransformConstraint): number { + return constraint.mixY; + } +} + +export class ToY extends ToProperty { + apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void { + if (local) { + if (!relative) value -= bone.ay; + bone.ay += value * mix; + } else { + if (!relative) value -= bone.worldY; + bone.worldY += value * mix; + } + } +} + +export class FromScaleX extends FromProperty { + value (target: Bone, local: boolean): number { + return local ? target.ascaleX : Math.sqrt(target.a * target.a + target.c * target.c); + } + + mix (constraint: TransformConstraint): number { + return constraint.mixScaleX; + } +} + +export class ToScaleX extends ToProperty { + apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void { + if (local) { + if (relative) + bone.ascaleX *= 1 + ((value - 1) * mix); + else if (bone.ascaleX != 0) // + bone.ascaleX = 1 + (value / bone.ascaleX - 1) * mix; + } else { + let s: number; + if (relative) + s = 1 + (value - 1) * mix; + else { + s = Math.sqrt(bone.a * bone.a + bone.c * bone.c); + if (s != 0) s = 1 + (value / s - 1) * mix; + } + bone.a *= s; + bone.c *= s; + } + } +} + +export class FromScaleY extends FromProperty { + value (target: Bone, local: boolean): number { + return local ? target.ascaleY : Math.sqrt(target.b * target.b + target.d * target.d); + } + + mix (constraint: TransformConstraint): number { + return constraint.mixScaleY; + } +} + +export class ToScaleY extends ToProperty { + apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void { + if (local) { + if (relative) + bone.ascaleY *= 1 + ((value - 1) * mix); + else if (bone.ascaleY != 0) // + bone.ascaleY = 1 + (value / bone.ascaleY - 1) * mix; + } else { + let s: number; + if (relative) + s = 1 + (value - 1) * mix; + else { + s = Math.sqrt(bone.b * bone.b + bone.d * bone.d); + if (s != 0) s = 1 + (value / s - 1) * mix; + } + bone.b *= s; + bone.d *= s; + } + } +} + +export class FromShearY extends FromProperty { + value (target: Bone, local: boolean): number { + return local ? target.ashearY : (Math.atan2(target.d, target.b) - Math.atan2(target.c, target.a)) * MathUtils.radDeg - 90; + } + + mix (constraint: TransformConstraint): number { + return constraint.mixShearY; + } +} + +export class ToShearY extends ToProperty { + apply (bone: Bone, value: number, local: boolean, relative: boolean, mix: number): void { + if (local) { + if (!relative) value -= bone.ashearY; + bone.ashearY += value * mix; + } else { + const b = bone.b, d = bone.d, by = Math.atan2(d, b); + value = (value + 90) * MathUtils.degRad; + if (relative) + value -= MathUtils.PI / 2; + else { + value -= by - Math.atan2(bone.c, bone.a); + if (value > MathUtils.PI) + value -= MathUtils.PI2; + else if (value < -MathUtils.PI) // + value += MathUtils.PI2; + } + value = by + value * mix; + const s = Math.sqrt(b * b + d * d); + bone.b = Math.cos(value) * s; + bone.d = Math.sin(value) * s; + } + } +} \ No newline at end of file