[ts] Port of Transform constraint property mapping.

This commit is contained in:
Davide Tantillo 2025-04-01 15:22:16 +02:00
parent 457be16385
commit 4eeb2b62cb
5 changed files with 347 additions and 234 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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<FromProperty> = [];
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<ToProperty> = [];
/** 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;
}
}
}