[haxe] Port to 4.3 (WIP)

This commit is contained in:
Davide Tantillo 2025-06-04 17:40:33 +02:00
parent 3b39aeebbf
commit 4daeefd5cf
93 changed files with 3696 additions and 3392 deletions

View File

@ -29,437 +29,33 @@
package spine;
/** Stores a bone's current pose.
/** The current pose for a bone, before constraints are applied.
*
* A bone has a local transform which is used to compute its world transform. A bone also has an applied transform, which is a
* local transform that can be applied to compute the world transform. The local transform and applied transform may differ if a
* constraint or application code modifies the world transform after it was computed from the local transform. */
class Bone implements Updatable {
class Bone extends PosedActive<BoneData, BoneLocal, BonePose> {
static public var yDown:Bool = false;
private var _data:BoneData;
private var _skeleton:Skeleton;
private var _parent:Bone;
private var _children:Array<Bone> = new Array<Bone>();
/** The local x translation. */
public var x:Float = 0;
/** The local y translation. */
public var y:Float = 0;
/** The local rotation in degrees, counter clockwise. */
public var rotation:Float = 0;
/** The local scaleX. */
public var scaleX:Float = 0;
/** The local scaleY. */
public var scaleY:Float = 0;
/** The local shearX. */
public var shearX:Float = 0;
/** The local shearY. */
public var shearY:Float = 0;
/** The applied local x translation. */
public var ax:Float = 0;
/** The applied local y translation. */
public var ay:Float = 0;
/** The applied local rotation in degrees, counter clockwise. */
public var arotation:Float = 0;
/** The applied local scaleX. */
public var ascaleX:Float = 0;
/** The applied local scaleY. */
public var ascaleY:Float = 0;
/** The applied local shearX. */
public var ashearX:Float = 0;
/** The applied local shearY. */
public var ashearY:Float = 0;
/** Part of the world transform matrix for the X axis. If changed, updateAppliedTransform() should be called. */
public var a:Float = 0;
/** Part of the world transform matrix for the Y axis. If changed, updateAppliedTransform() should be called. */
public var b:Float = 0;
/** Part of the world transform matrix for the X axis. If changed, updateAppliedTransform() should be called. */
public var c:Float = 0;
/** Part of the world transform matrix for the Y axis. If changed, updateAppliedTransform() should be called. */
public var d:Float = 0;
/** The world X position. If changed, updateAppliedTransform() should be called. */
public var worldX:Float = 0;
/** The world Y position. If changed, updateAppliedTransform() should be called. */
public var worldY:Float = 0;
/** Determines how parent world transforms affect this bone. */
public var inherit:Inherit = Inherit.normal;
public var sorted:Bool = false;
public var active:Bool = false;
/** The bone's setup pose data. */
public var data(get, never):BoneData;
private function get_data():BoneData {
return _data;
}
/** The skeleton this bone belongs to. */
public var skeleton(get, never):Skeleton;
private function get_skeleton():Skeleton {
return _skeleton;
}
/** The parent bone, or null if this is the root bone. */
public var parent(get, never):Bone;
private function get_parent():Bone {
return _parent;
}
public final parent:Bone = null;
/** The immediate children of this bone. */
public var children(get, never):Array<Bone>;
public final children:Array<Bone> = new Array<Bone>();
private function get_children():Array<Bone> {
return _children;
public var sorted = false;
public function new (data:BoneData, parent:Bone) {
super(data, new BonePose(), new BonePose());
this.parent = parent;
applied.bone = this;
constrained.bone = this;
}
/** Copy constructor. Does not copy the children bones. */
public function new(data:BoneData, skeleton:Skeleton, parent:Bone) {
if (data == null)
throw new SpineException("data cannot be null.");
if (skeleton == null)
throw new SpineException("skeleton cannot be null.");
_data = data;
_skeleton = skeleton;
_parent = parent;
setToSetupPose();
}
public function isActive():Bool {
return active;
}
/** Computes the world transform using the parent bone and this bone's local applied transform. */
public function update(physics:Physics):Void {
updateWorldTransformWith(ax, ay, arotation, ascaleX, ascaleY, ashearX, ashearY);
}
/** Computes the world transform using the parent bone and this bone's local transform.
*
* See updateWorldTransformWith(). */
public function updateWorldTransform():Void {
updateWorldTransformWith(x, y, rotation, scaleX, scaleY, shearX, shearY);
}
/** Computes the world transform using the parent bone and the specified local transform. The applied transform is set to the
* specified local transform. Child bones are not updated.
*
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
*/
public function updateWorldTransformWith(x:Float, y:Float, rotation:Float, scaleX:Float, scaleY:Float, shearX:Float, shearY:Float):Void {
ax = x;
ay = y;
arotation = rotation;
ascaleX = scaleX;
ascaleY = scaleY;
ashearX = shearX;
ashearY = shearY;
var la:Float = 0;
var lb:Float = 0;
var lc:Float = 0;
var ld:Float = 0;
var sin:Float = 0;
var cos:Float = 0;
var s:Float = 0;
var sx:Float = skeleton.scaleX;
var sy:Float = skeleton.scaleY;
var parent:Bone = _parent;
if (parent == null) {
// Root bone.
var rx:Float = (rotation + shearX) * MathUtils.degRad;
var ry:Float = (rotation + 90 + shearY) * MathUtils.degRad;
a = Math.cos(rx) * scaleX * sx;
b = Math.cos(ry) * scaleY * sx;
c = Math.sin(rx) * scaleX * sy;
d = Math.sin(ry) * scaleY * sy;
worldX = x * sx + skeleton.x;
worldY = y * sy + skeleton.y;
return;
}
var pa:Float = parent.a,
pb:Float = parent.b,
pc:Float = parent.c,
pd:Float = parent.d;
worldX = pa * x + pb * y + parent.worldX;
worldY = pc * x + pd * y + parent.worldY;
switch (inherit) {
case Inherit.normal:
var rx:Float = (rotation + shearX) * MathUtils.degRad;
var ry:Float = (rotation + 90 + shearY) * MathUtils.degRad;
la = Math.cos(rx) * scaleX;
lb = Math.cos(ry) * scaleY;
lc = Math.sin(rx) * scaleX;
ld = Math.sin(ry) * scaleY;
a = pa * la + pb * lc;
b = pa * lb + pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
return;
case Inherit.onlyTranslation:
var rx:Float = (rotation + shearX) * MathUtils.degRad;
var ry:Float = (rotation + 90 + shearY) * MathUtils.degRad;
a = Math.cos(rx) * scaleX;
b = Math.cos(ry) * scaleY;
c = Math.sin(rx) * scaleX;
d = Math.sin(ry) * scaleY;
case Inherit.noRotationOrReflection:
var sx:Float = 1 / skeleton.scaleX;
var sy:Float = 1 / skeleton.scaleY;
pa *= sx;
pc *= sy;
s = pa * pa + pc * pc;
var prx:Float = 0;
if (s > 0.0001) {
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
pb = pc * s;
pd = pa * s;
prx = Math.atan2(pc, pa) * MathUtils.radDeg;
} else {
pa = 0;
pc = 0;
prx = 90 - Math.atan2(pd, pb) * MathUtils.radDeg;
}
var rx:Float = (rotation + shearX - prx) * MathUtils.degRad;
var ry:Float = (rotation + shearY - prx + 90) * MathUtils.degRad;
la = Math.cos(rx) * scaleX;
lb = Math.cos(ry) * scaleY;
lc = Math.sin(rx) * scaleX;
ld = Math.sin(ry) * scaleY;
a = pa * la - pb * lc;
b = pa * lb - pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
case Inherit.noScale, Inherit.noScaleOrReflection:
rotation *= MathUtils.degRad;
cos = Math.cos(rotation);
sin = Math.sin(rotation);
var za:Float = (pa * cos + pb * sin) / sx;
var zc:Float = (pc * cos + pd * sin) / sy;
s = Math.sqrt(za * za + zc * zc);
if (s > 0.00001)
s = 1 / s;
za *= s;
zc *= s;
s = Math.sqrt(za * za + zc * zc);
if (inherit == Inherit.noScale && ((pa * pd - pb * pc < 0) != ((sx < 0) != (sy < 0)))) {
s = -s;
}
rotation = Math.PI / 2 + Math.atan2(zc, za);
var zb:Float = Math.cos(rotation) * s;
var zd:Float = Math.sin(rotation) * s;
shearX *= MathUtils.degRad;
shearY = (90 + shearY) * MathUtils.degRad;
la = Math.cos(shearX) * scaleX;
lb = Math.cos(shearY) * scaleY;
lc = Math.sin(shearX) * scaleX;
ld = Math.sin(shearY) * scaleY;
a = za * la + zb * lc;
b = za * lb + zb * ld;
c = zc * la + zd * lc;
d = zc * lb + zd * ld;
}
a *= sx;
b *= sx;
c *= sy;
d *= sy;
}
/** Sets this bone's local transform to the setup pose. */
public function setToSetupPose():Void {
x = data.x;
y = data.y;
rotation = data.rotation;
scaleX = data.scaleX;
scaleY = data.scaleY;
shearX = data.shearX;
shearY = data.shearY;
inherit = data.inherit;
}
/** Computes the applied transform values from the world transform.
*
* If the world transform is modified (by a constraint, rotateWorld(), etc) then this method should be called so
* the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another
* constraint).
*
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
public function updateAppliedTransform():Void {
var parent:Bone = parent;
if (parent == null) {
ax = worldX - skeleton.x;
ay = worldY - skeleton.y;
arotation = Math.atan2(c, a) * MathUtils.radDeg;
ascaleX = Math.sqrt(a * a + c * c);
ascaleY = Math.sqrt(b * b + d * d);
ashearX = 0;
ashearY = Math.atan2(a * b + c * d, a * d - b * c) * MathUtils.radDeg;
return;
}
var pa:Float = parent.a,
pb:Float = parent.b,
pc:Float = parent.c,
pd:Float = parent.d;
var pid:Float = 1 / (pa * pd - pb * pc);
var ia:Float = pd * pid,
ib:Float = pb * pid,
ic:Float = pc * pid,
id:Float = pa * pid;
var dx:Float = worldX - parent.worldX,
dy:Float = worldY - parent.worldY;
ax = (dx * ia - dy * ib);
ay = (dy * id - dx * ic);
var ra:Float, rb:Float, rc:Float, rd:Float;
if (inherit == Inherit.onlyTranslation) {
ra = a;
rb = b;
rc = c;
rd = d;
} else {
switch (inherit) {
case Inherit.noRotationOrReflection:
var s:Float = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
case Inherit.noScale | Inherit.noScaleOrReflection:
var cos:Float = MathUtils.cosDeg(rotation), sin:Float = MathUtils.sinDeg(rotation);
pa = (pa * cos + pb * sin) / skeleton.scaleX;
pc = (pc * cos + pd * sin) / skeleton.scaleY;
var s:Float = Math.sqrt(pa * pa + pc * pc);
if (s > 0.00001) s = 1 / s;
pa *= s;
pc *= s;
s = Math.sqrt(pa * pa + pc * pc);
if (inherit == Inherit.noScale && pid < 0 != ((skeleton.scaleX < 0) != (skeleton.scaleY < 0))) s = -s;
var r:Float = MathUtils.PI / 2 + Math.atan2(pc, pa);
pb = Math.cos(r) * s;
pd = Math.sin(r) * s;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
ic = pc * pid;
id = pa * pid;
}
ra = ia * a - ib * c;
rb = ia * b - ib * d;
rc = id * c - ic * a;
rd = id * d - ic * b;
}
ashearX = 0;
ascaleX = Math.sqrt(ra * ra + rc * rc);
if (scaleX > 0.0001) {
var det:Float = ra * rd - rb * rc;
ascaleY = det / ascaleX;
ashearY = -Math.atan2(ra * rb + rc * rd, det) * MathUtils.radDeg;
arotation = Math.atan2(rc, ra) * MathUtils.radDeg;
} else {
ascaleX = 0;
ascaleY = Math.sqrt(rb * rb + rd * rd);
ashearY = 0;
arotation = 90 - Math.atan2(rd, rb) * MathUtils.radDeg;
}
}
/** The world rotation for the X axis, calculated using a and c. */
public var worldRotationX(get, never):Float;
private function get_worldRotationX():Float {
return Math.atan2(c, a) * MathUtils.radDeg;
}
/** The world rotation for the Y axis, calculated using b and d. */
public var worldRotationY(get, never):Float;
private function get_worldRotationY():Float {
return Math.atan2(d, b) * MathUtils.radDeg;
}
/** The magnitude (always positive) of the world scale X, calculated using a and c. */
public var worldScaleX(get, never):Float;
private function get_worldScaleX():Float {
return Math.sqrt(a * a + c * c);
}
/** The magnitude (always positive) of the world scale Y, calculated using b and d. */
public var worldScaleY(get, never):Float;
private function get_worldScaleY():Float {
return Math.sqrt(b * b + d * d);
}
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public function worldToParent(world: Array<Float>):Array<Float> {
if (world == null)
throw new SpineException("world cannot be null.");
return parent == null ? world : parent.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public function parentToWorld(world: Array<Float>):Array<Float> {
if (world == null)
throw new SpineException("world cannot be null.");
return parent == null ? world : parent.localToWorld(world);
}
/** Transforms a point from world coordinates to the bone's local coordinates. */
public function worldToLocal(world:Array<Float>):Array<Float> {
var a:Float = a, b:Float = b, c:Float = c, d:Float = d;
var invDet:Float = 1 / (a * d - b * c);
var x:Float = world[0] - worldX, y:Float = world[1] - worldY;
world[0] = (x * d * invDet - y * b * invDet);
world[1] = (y * a * invDet - x * c * invDet);
return world;
}
/** Transforms a point from the bone's local coordinates to world coordinates. */
public function localToWorld(local:Array<Float>):Array<Float> {
var localX:Float = local[0], localY:Float = local[1];
local[0] = localX * a + localY * b + worldX;
local[1] = localX * c + localY * d + worldY;
return local;
}
/** Transforms a world rotation to a local rotation. */
public function worldToLocalRotation(worldRotation:Float):Float {
var sin:Float = MathUtils.sinDeg(worldRotation),
cos:Float = MathUtils.cosDeg(worldRotation);
return Math.atan2(a * sin - c * cos, d * cos - b * sin) * MathUtils.radDeg + rotation - shearX;
}
/** Transforms a local rotation to a world rotation. */
public function localToWorldRotation(localRotation:Float):Float {
localRotation -= rotation - shearX;
var sin:Float = MathUtils.sinDeg(localRotation),
cos:Float = MathUtils.cosDeg(localRotation);
return Math.atan2(cos * c + sin * d, cos * a + sin * b) * MathUtils.radDeg;
}
/** Rotates the world transform the specified amount.
*
* After changes are made to the world transform, updateAppliedTransform() should be called and
* update() will need to be called on any child bones, recursively. */
public function rotateWorld(degrees:Float):Void {
degrees *= MathUtils.degRad;
var sin:Float = Math.sin(degrees), cos:Float = Math.cos(degrees);
var ra:Float = a, rb:Float = b;
a = cos * ra - sin * c;
b = cos * rb - sin * d;
c = sin * ra + cos * c;
d = sin * rb + cos * d;
}
public function toString():String {
return data.name;
/** Copy method. Does not copy the children bones. */
public function copy(bone:Bone, parent:Bone):Bone {
var copy = new Bone(bone.data, parent);
pose.set(bone.pose);
return copy;
}
}

View File

@ -29,75 +29,41 @@
package spine;
/** Stores the setup pose for a spine.Bone. */
class BoneData {
private var _index:Int;
private var _name:String;
private var _parent:BoneData;
/** The bone's length. */
public var length:Float = 0;
/** The local x translation. */
public var x:Float = 0;
/** The local y translation. */
public var y:Float = 0;
/** The local rotation in degrees, counter clockwise. */
public var rotation:Float = 0;
/** The local scaleX. */
public var scaleX:Float = 1;
/** The local scaleY. */
public var scaleY:Float = 1;
/** The local shearX. */
public var shearX:Float = 0;
/** The local shearY. */
public var shearY:Float = 0;
/** Determines how parent world transforms affect this bone. */
public var inherit:Inherit = Inherit.normal;
/** When true, spine.Skeleton.updateWorldTransform() only updates this bone if the spine.Skeleton.getSkin() contains
* this bone.
* @see spine.Skin.getBones() */
public var skinRequired:Bool = false;
/** The color of the bone as it was in Spine, or a default color if nonessential data was not exported. Bones are not usually
* rendered at runtime. */
public var color:Color = new Color(0, 0, 0, 0);
/** The bone icon as it was in Spine, or null if nonessential data was not exported. */
public var icon:String;
/** False if the bone was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
public var visible:Bool = false;
/** Copy constructor. */
public function new(index:Int, name:String, parent:BoneData) {
if (index < 0)
throw new SpineException("index must be >= 0");
if (name == null)
throw new SpineException("name cannot be null.");
_index = index;
_name = name;
_parent = parent;
}
/** The setup pose for a bone. */
class BoneData extends PosedData<BoneLocal> {
/** The index of the bone in spine.Skeleton.getBones(). */
public var index(get, never):Int;
public final index:Int;
private function get_index():Int {
return _index;
public final parent:BoneData = null;
/** The bone's length. */
public var length = 0.;
// Nonessential.
/** The color of the bone as it was in Spine, or a default color if nonessential data was not exported. Bones are not usually
* rendered at runtime. */
public var color = new Color(0, 0, 0, 0);
/** The bone icon as it was in Spine, or null if nonessential data was not exported. */
public var icon:String = null;
/** False if the bone was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
public var visible = false;
public function new (index:Int, name:String, parent:BoneData) {
super(name, new BoneLocal());
if (index < 0) throw new SpineException("index must be >= 0.");
if (name == null) throw new SpineException("name cannot be null.");
this.index = index;
this.parent = parent;
}
/** The name of the bone, which is unique across all bones in the skeleton. */
public var name(get, never):String;
private function get_name():String {
return _name;
}
/** @return May be null. */
public var parent(get, never):BoneData;
private function get_parent():BoneData {
return _parent;
}
public function toString():String {
return _name;
/** Copy method. */
public function copy(data:BoneData, parent:BoneData) {
var copy = new BoneData(data.index, data.name, parent);
length = data.length;
setup.set(data.setup);
return copy;
}
}

View File

@ -0,0 +1,79 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores a bone's local pose. */
class BoneLocal implements Pose<BoneLocal> {
/** The local x translation. */
public var x:Float = 0;
/** The local y translation. */
public var y:Float = 0;
/** The local rotation in degrees, counter clockwise. */
public var rotation:Float = 0;
/** The local scaleX. */
public var scaleX:Float = 0;
/** The local scaleY. */
public var scaleY:Float = 0;
/** The local shearX. */
public var shearX:Float = 0;
/** The local shearY. */
public var shearY:Float = 0;
/** Determines how parent world transforms affect this bone. */
public var inherit(default, set):Inherit;
function set_inherit (value:Inherit):Inherit {
if (value == null) throw new SpineException("inherit cannot be null.");
inherit = value;
return value;
}
public function new () {
}
public function set (pose:BoneLocal):Void {
if (pose == null) throw new SpineException("pose cannot be null.");
x = pose.x;
y = pose.y;
rotation = pose.rotation;
scaleX = pose.scaleX;
scaleY = pose.scaleY;
shearX = pose.shearX;
shearY = pose.shearY;
inherit = pose.inherit;
}
}

View File

@ -0,0 +1,381 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** The applied pose for a bone. This is the {@link Bone} pose with constraints applied and the world transform computed by
* Skeleton.updateWorldTransform(Physics). */
class BonePose extends BoneLocal implements Update {
public var bone:Bone;
/** Part of the world transform matrix for the X axis. If changed, updateAppliedTransform() should be called. */
public var a:Float = 0;
/** Part of the world transform matrix for the Y axis. If changed, updateAppliedTransform() should be called. */
public var b:Float = 0;
/** Part of the world transform matrix for the X axis. If changed, updateAppliedTransform() should be called. */
public var c:Float = 0;
/** Part of the world transform matrix for the Y axis. If changed, updateAppliedTransform() should be called. */
public var d:Float = 0;
/** The world X position. If changed, updateAppliedTransform() should be called. */
public var worldX:Float = 0;
/** The world Y position. If changed, updateAppliedTransform() should be called. */
public var worldY:Float = 0;
public var world:Int;
public var local:Int;
// public function new () {
// super();
// }
/** Called by Skeleton.updateCache() to compute the world transform, if needed. */
public function update (skeleton:Skeleton, physics:Physics):Void {
if (world != skeleton._update) updateWorldTransform(skeleton);
}
/** Computes the world transform using the parent bone's applied pose and this pose. Child bones are not updated.
*
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
*/
public function updateWorldTransform(skeleton:Skeleton):Void {
if (local == skeleton._update)
updateLocalTransform(skeleton);
else
world = skeleton._update;
if (bone.parent == null) { // Root bone.
var sx = skeleton.scaleX, sy = skeleton.scaleY;
var rx = (rotation + shearX) * MathUtils.degRad;
var ry = (rotation + 90 + shearY) * MathUtils.degRad;
a = Math.cos(rx) * scaleX * sx;
b = Math.cos(ry) * scaleY * sx;
c = Math.sin(rx) * scaleX * sy;
d = Math.sin(ry) * scaleY * sy;
worldX = x * sx + skeleton.x;
worldY = y * sy + skeleton.y;
return;
}
var parent = bone.parent.applied;
var pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
worldX = pa * x + pb * y + parent.worldX;
worldY = pc * x + pd * y + parent.worldY;
switch (inherit) {
case Inherit.normal:
var rx = (rotation + shearX) * MathUtils.degRad;
var ry = (rotation + 90 + shearY) * MathUtils.degRad;
var la = Math.cos(rx) * scaleX;
var lb = Math.cos(ry) * scaleY;
var lc = Math.sin(rx) * scaleX;
var ld = Math.sin(ry) * scaleY;
a = pa * la + pb * lc;
b = pa * lb + pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
return;
case Inherit.onlyTranslation:
var rx = (rotation + shearX) * MathUtils.degRad;
var ry = (rotation + 90 + shearY) * MathUtils.degRad;
a = Math.cos(rx) * scaleX;
b = Math.cos(ry) * scaleY;
c = Math.sin(rx) * scaleX;
d = Math.sin(ry) * scaleY;
case Inherit.noRotationOrReflection:
var sx = 1 / skeleton.scaleX, sy = 1 / skeleton.scaleY;
pa *= sx;
pc *= sy;
var s = pa * pa + pc * pc, prx:Float;
if (s > 0.0001) {
s = Math.abs(pa * pd * sy - pb * sx * pc) / s;
pb = pc * s;
pd = pa * s;
prx = MathUtils.atan2Deg(pc, pa);
} else {
pa = 0;
pc = 0;
prx = 90 - MathUtils.atan2Deg(pd, pb);
}
var rx = (rotation + shearX - prx) * MathUtils.degRad;
var ry = (rotation + shearY - prx + 90) * MathUtils.degRad;
var la = Math.cos(rx) * scaleX;
var lb = Math.cos(ry) * scaleY;
var lc = Math.sin(rx) * scaleX;
var ld = Math.sin(ry) * scaleY;
a = pa * la - pb * lc;
b = pa * lb - pb * ld;
c = pc * la + pd * lc;
d = pc * lb + pd * ld;
case Inherit.noScale, Inherit.noScaleOrReflection:
rotation *= MathUtils.degRad;
var cos = Math.cos(rotation);
var sin = Math.sin(rotation);
var za = (pa * cos + pb * sin) / skeleton.scaleX;
var zc = (pc * cos + pd * sin) / skeleton.scaleY;
var s = Math.sqrt(za * za + zc * zc);
if (s > 0.00001) s = 1 / s;
za *= s;
zc *= s;
s = Math.sqrt(za * za + zc * zc);
if (inherit == Inherit.noScale && ((pa * pd - pb * pc < 0) != ((skeleton.scaleX < 0) != (skeleton.scaleY < 0)))) s = -s;
rotation = Math.PI / 2 + Math.atan2(zc, za);
var zb:Float = Math.cos(rotation) * s;
var zd:Float = Math.sin(rotation) * s;
shearX *= MathUtils.degRad;
shearY = (90 + shearY) * MathUtils.degRad;
var la = Math.cos(shearX) * scaleX;
var lb = Math.cos(shearY) * scaleY;
var lc = Math.sin(shearX) * scaleX;
var ld = Math.sin(shearY) * scaleY;
a = za * la + zb * lc;
b = za * lb + zb * ld;
c = zc * la + zd * lc;
d = zc * lb + zd * ld;
}
a *= skeleton.scaleX;
b *= skeleton.scaleX;
c *= skeleton.scaleY;
d *= skeleton.scaleY;
}
/** Computes the applied transform values from the world transform.
*
* If the world transform is modified (by a constraint, rotateWorld(), etc) then this method should be called so
* the applied transform matches the world transform. The applied transform may be needed by other code (eg to apply another
* constraint).
*
* Some information is ambiguous in the world transform, such as -1,-1 scale versus 180 rotation. The applied transform after
* calling this method is equivalent to the local transform used to compute the world transform, but may not be identical. */
public function updateLocalTransform(skeleton:Skeleton):Void {
local = 0;
world = skeleton._update;
if (bone.parent == null) {
x = worldX - skeleton.x;
y = worldY - skeleton.y;
rotation = MathUtils.atan2Deg(c, a);
scaleX = Math.sqrt(a * a + c * c);
scaleY = Math.sqrt(b * b + d * d);
shearX = 0;
shearY = MathUtils.atan2Deg(a * b + c * d, a * d - b * c);
return;
}
var parent = bone.parent.applied;
var pa = parent.a, pb = parent.b, pc = parent.c, pd = parent.d;
var pid:Float = 1 / (pa * pd - pb * pc);
var ia = pd * pid, ib = pb * pid, ic = pc * pid, id = pa * pid;
var dx = worldX - parent.worldX, dy = worldY - parent.worldY;
x = (dx * ia - dy * ib);
y = (dy * id - dx * ic);
var ra:Float, rb:Float, rc:Float, rd:Float;
if (inherit == Inherit.onlyTranslation) {
ra = a;
rb = b;
rc = c;
rd = d;
} else {
switch (inherit) {
case Inherit.noRotationOrReflection:
var s = Math.abs(pa * pd - pb * pc) / (pa * pa + pc * pc);
pb = -pc * skeleton.scaleX * s / skeleton.scaleY;
pd = pa * skeleton.scaleY * s / skeleton.scaleX;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
case Inherit.noScale, Inherit.noScaleOrReflection:
var r = rotation * MathUtils.degRad, cos = Math.cos(rotation), sin = Math.sin(rotation);
pa = (pa * cos + pb * sin) / skeleton.scaleX;
pc = (pc * cos + pd * sin) / skeleton.scaleY;
var s = Math.sqrt(pa * pa + pc * pc);
if (s > 0.00001) s = 1 / s;
pa *= s;
pc *= s;
s = Math.sqrt(pa * pa + pc * pc);
if (inherit == Inherit.noScale && (pid < 0 != ((skeleton.scaleX < 0) != (skeleton.scaleY < 0)))) s = -s;
r = MathUtils.PI / 2 + Math.atan2(pc, pa);
pb = Math.cos(r) * s;
pd = Math.sin(r) * s;
pid = 1 / (pa * pd - pb * pc);
ia = pd * pid;
ib = pb * pid;
ic = pc * pid;
id = pa * pid;
}
ra = ia * a - ib * c;
rb = ia * b - ib * d;
rc = id * c - ic * a;
rd = id * d - ic * b;
}
shearX = 0;
scaleX = Math.sqrt(ra * ra + rc * rc);
if (scaleX > 0.0001) {
var det = ra * rd - rb * rc;
scaleY = det / scaleX;
shearY = -MathUtils.atan2Deg(ra * rb + rc * rd, det);
rotation = MathUtils.atan2Deg(rc, ra);
} else {
scaleX = 0;
scaleY = Math.sqrt(rb * rb + rd * rd);
shearY = 0;
rotation = 90 - MathUtils.atan2Deg(rd, rb);
}
}
/** If the world transform has been modified and the local transform no longer matches, {@link #updateLocalTransform(Skeleton)}
* is called. */
public function validateLocalTransform (skeleton: Skeleton) {
if (local == skeleton._update) updateLocalTransform(skeleton);
}
public function modifyLocal (skeleton: Skeleton) {
if (local == skeleton._update) updateLocalTransform(skeleton);
world = 0;
resetWorld(skeleton._update);
}
public function modifyWorld (update:Int) {
local = update;
world = update;
resetWorld(update);
}
public function resetWorld (update:Int) {
var children = bone.children;
for (i in 0...bone.children.length) {
var child = children[i].applied;
if (child.world == update) {
child.world = 0;
child.local = 0;
child.resetWorld(update);
}
}
}
/** The world rotation for the X axis, calculated using a and c. */
public var worldRotationX(get, never):Float;
private function get_worldRotationX():Float {
return MathUtils.atan2Deg(c, a);
}
/** The world rotation for the Y axis, calculated using b and d. */
public var worldRotationY(get, never):Float;
private function get_worldRotationY():Float {
return MathUtils.atan2Deg(d, b);
}
/** The magnitude (always positive) of the world scale X, calculated using a and c. */
public var worldScaleX(get, never):Float;
private function get_worldScaleX():Float {
return Math.sqrt(a * a + c * c);
}
/** The magnitude (always positive) of the world scale Y, calculated using b and d. */
public var worldScaleY(get, never):Float;
private function get_worldScaleY():Float {
return Math.sqrt(b * b + d * d);
}
/** Transforms a point from world coordinates to the bone's local coordinates. */
public function worldToLocal(world:Array<Float>):Array<Float> {
var a:Float = a, b:Float = b, c:Float = c, d:Float = d;
var invDet:Float = 1 / (a * d - b * c);
var x:Float = world[0] - worldX, y:Float = world[1] - worldY;
world[0] = (x * d * invDet - y * b * invDet);
world[1] = (y * a * invDet - x * c * invDet);
return world;
}
/** Transforms a point from the bone's local coordinates to world coordinates. */
public function localToWorld(local:Array<Float>):Array<Float> {
var localX:Float = local[0], localY:Float = local[1];
local[0] = localX * a + localY * b + worldX;
local[1] = localX * c + localY * d + worldY;
return local;
}
/** Transforms a point from world coordinates to the parent bone's local coordinates. */
public function worldToParent(world: Array<Float>):Array<Float> {
if (world == null)
throw new SpineException("world cannot be null.");
return bone.parent == null ? world : bone.parent.applied.worldToLocal(world);
}
/** Transforms a point from the parent bone's coordinates to world coordinates. */
public function parentToWorld(world: Array<Float>):Array<Float> {
if (world == null)
throw new SpineException("world cannot be null.");
return bone.parent == null ? world : bone.parent.applied.localToWorld(world);
}
/** Transforms a world rotation to a local rotation. */
public function worldToLocalRotation(worldRotation:Float):Float {
var sin:Float = MathUtils.sinDeg(worldRotation),
cos:Float = MathUtils.cosDeg(worldRotation);
return Math.atan2(a * sin - c * cos, d * cos - b * sin) * MathUtils.radDeg + rotation - shearX;
}
/** Transforms a local rotation to a world rotation. */
public function localToWorldRotation(localRotation:Float):Float {
localRotation -= rotation - shearX;
var sin:Float = MathUtils.sinDeg(localRotation),
cos:Float = MathUtils.cosDeg(localRotation);
return Math.atan2(cos * c + sin * d, cos * a + sin * b) * MathUtils.radDeg;
}
/** Rotates the world transform the specified amount.
*
* After changes are made to the world transform, updateAppliedTransform() should be called and
* update() will need to be called on any child bones, recursively. */
public function rotateWorld(degrees:Float):Void {
degrees *= MathUtils.degRad;
var sin:Float = Math.sin(degrees), cos:Float = Math.cos(degrees);
var ra:Float = a, rb:Float = b;
a = cos * ra - sin * c;
b = cos * rb - sin * d;
c = sin * ra + cos * c;
d = sin * rb + cos * d;
}
public function toString ():String {
return bone.data.name;
}
}

View File

@ -0,0 +1,49 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
abstract class Constraint< //
T:Constraint<T, D, P>, //
D:ConstraintData<T, P>, //
P:Pose<Any>> //
extends PosedActive<D, P, P> implements Update {
public function new (data:D, pose:P, constrained:P) {
super(data, pose, constrained);
}
public abstract function copy (skeleton:Skeleton):T;
public abstract function sort (skeleton:Skeleton):Void;
public function isSourceActive ():Bool {
return true;
}
}

View File

@ -30,24 +30,14 @@
package spine;
/** The base class for all constraint datas. */
class ConstraintData {
/** The constraint's name, which is unique across all constraints in the skeleton of the same type. */
public var name:String;
/** The ordinal of this constraint for the order a skeleton's constraints will be applied by
* spine.Skeleton.updateWorldTransform(). */
public var order:Int = 0;
/** When true, spine.Skeleton.updateWorldTransform() only updates this constraint if the spine.Skeleton.getSkin()
* contains this constraint.
* @see spine.Skin.getConstraints() */
public var skinRequired:Bool = false;
abstract class ConstraintData< //
T:Constraint<Dynamic, Dynamic, Dynamic>, //
P:Pose<Any>> //
extends PosedData<P> {
function new(name:String, order:Int, skinRequired:Bool) {
this.name = name;
this.order = order;
this.skinRequired = skinRequired;
function new(name:String, setup:P) {
super(name, setup);
}
public function toString():String {
return name;
}
public abstract function create (skeleton:Skeleton):T;
}

View File

@ -30,38 +30,32 @@
package spine;
/** Stores the current pose values for an Event.
*
*
* @see spine.Timeline
* @see spine.Timeline.apply()
* @see spine.AnimationStateListener.event()
* @see https://esotericsoftware.com/spine-events Events in the Spine User Guide
*/
class Event {
private var _data:EventData;
/** The event's setup pose data. */
public var data:EventData;
/** The animation time this event was keyed. */
public var time:Float = 0;
public var intValue:Int = 0;
public var floatValue:Float = 0;
public var time = 0.;
public var intValue = 0;
public var floatValue = 0;
public var stringValue:String;
public var volume:Float = 1;
public var balance:Float = 0;
public var volume = 1.;
public var balance = 0.;
public function new(time:Float, data:EventData) {
if (data == null)
throw new SpineException("data cannot be null.");
if (data == null) throw new SpineException("data cannot be null.");
this.time = time;
_data = data;
}
/** The event's setup pose data. */
public var data(get, never):EventData;
private function get_data():EventData {
return _data;
this.data = data;
}
public function toString():String {
return _data.name != null ? _data.name : "Event?";
return data.name != null ? data.name : "Event?";
}
}

View File

@ -33,171 +33,136 @@ package spine;
* the last bone is as close to the target bone as possible.
*
* @see https://esotericsoftware.com/spine-ik-constraints IK constraints in the Spine User Guide */
class IkConstraint implements Updatable {
private var _data:IkConstraintData;
class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkConstraintPose> {
/** The 1 or 2 bones that will be modified by this IK constraint. */
public final bones:Array<BonePose>;
/** The bones that will be modified by this IK constraint. */
public var bones:Array<Bone>;
/** The bone that is the IK target. */
public var target:Bone;
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
public var bendDirection:Int = 0;
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
public var compress:Bool = false;
/** When true and the target is out of range, the parent bone is scaled to reach it.
*
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if getSoftness() is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
public var stretch:Bool = false;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
*
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
public var mix:Float = 0;
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
* will not straighten completely until the target is this far out of range. */
public var softness:Float = 0;
public var active:Bool = false;
public var target(default, set):Bone;
/** Copy constructor. */
public function new(data:IkConstraintData, skeleton:Skeleton) {
if (data == null)
throw new SpineException("data cannot be null.");
if (skeleton == null)
throw new SpineException("skeleton cannot be null.");
_data = data;
super(data, new IkConstraintPose(), new IkConstraintPose());
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
bones = new Array<Bone>();
for (boneData in data.bones) {
bones.push(skeleton.findBone(boneData.name));
}
target = skeleton.findBone(data.target.name);
mix = data.mix;
softness = data.softness;
bendDirection = data.bendDirection;
compress = data.compress;
stretch = data.stretch;
bones = new Array<BonePose>();
for (boneData in data.bones)
bones.push(skeleton.bones[boneData.index].constrained);
target = skeleton.bones[data.target.index];
}
public function isActive():Bool {
return active;
}
public function setToSetupPose () {
var data:IkConstraintData = _data;
mix = data.mix;
softness = data.softness;
bendDirection = data.bendDirection;
compress = data.compress;
stretch = data.stretch;
public function copy (skeleton:Skeleton) {
var copy = new IkConstraint(data, skeleton);
copy.pose.set(pose);
return copy;
}
/** Applies the constraint to the constrained bones. */
public function update(physics:Physics):Void {
if (mix == 0)
return;
public function update (skeleton:Skeleton, physics:Physics):Void {
var p = applied;
if (p.mix == 0) return;
var target = target.applied;
switch (bones.length) {
case 1:
apply1(bones[0], target.worldX, target.worldY, compress, stretch, _data.uniform, mix);
case 2:
apply2(bones[0], bones[1], target.worldX, target.worldY, bendDirection, stretch, _data.uniform, softness, mix);
case 1: apply1(skeleton, bones[0], target.worldX, target.worldY, p.compress, p.stretch, data.uniform, p.mix);
case 2: apply2(skeleton, bones[0], bones[1], target.worldX, target.worldY, p.bendDirection, p.stretch, data.uniform, p.softness, p.mix);
}
}
/** The IK constraint's setup pose data. */
public var data(get, never):IkConstraintData;
private function get_data():IkConstraintData {
return _data;
public function sort (skeleton:Skeleton) {
skeleton.sortBone(target);
var parent = bones[0].bone;
skeleton.sortBone(parent);
skeleton._updateCache.push(this);
parent.sorted = false;
skeleton.sortReset(parent.children);
skeleton.constrained(parent);
if (bones.length > 1) skeleton.constrained(bones[1].bone);
}
public function toString():String {
return _data.name != null ? _data.name : "IkConstraint?";
override public function isSourceActive () {
return target.active;
}
public function set_target (target:Bone):Bone {
if (target == null) throw new SpineException("target cannot be null.");
this.target = target;
return target;
}
/** Applies 1 bone IK. The target is specified in the world coordinate system. */
static public function apply1(bone:Bone, targetX:Float, targetY:Float, compress:Bool, stretch:Bool, uniform:Bool, alpha:Float):Void {
var p:Bone = bone.parent;
var pa:Float = p.a, pb:Float = p.b, pc:Float = p.c, pd:Float = p.d;
var rotationIK:Float = -bone.ashearX - bone.arotation,
tx:Float = 0,
ty:Float = 0;
static public function apply1(skeleton:Skeleton, bone:BonePose, targetX:Float, targetY:Float, compress:Bool, stretch:Bool,
uniform:Bool, mix:Float) {
if (bone == null) throw new SpineException("bone cannot be null.");
bone.modifyLocal(skeleton);
var p = bone.bone.parent.applied;
var pa = p.a, pb = p.b, pc = p.c, pd = p.d;
var rotationIK = -bone.shearX - bone.rotation, tx = 0., ty = 0.;
function switchDefault() {
var x:Float = targetX - p.worldX, y:Float = targetY - p.worldY;
var d:Float = pa * pd - pb * pc;
var x = targetX - p.worldX, y = targetY - p.worldY;
var d = pa * pd - pb * pc;
if (Math.abs(d) <= 0.0001) {
tx = 0;
ty = 0;
} else {
tx = (x * pd - y * pb) / d - bone.ax;
ty = (y * pa - x * pc) / d - bone.ay;
tx = (x * pd - y * pb) / d - bone.x;
ty = (y * pa - x * pc) / d - bone.y;
}
}
switch (bone.inherit) {
case Inherit.onlyTranslation:
tx = (targetX - bone.worldX) * MathUtils.signum(bone.skeleton.scaleX);
ty = (targetY - bone.worldY) * MathUtils.signum(bone.skeleton.scaleY);
tx = (targetX - bone.worldX) * MathUtils.signum(skeleton.scaleX);
ty = (targetY - bone.worldY) * MathUtils.signum(skeleton.scaleY);
case Inherit.noRotationOrReflection:
var s = Math.abs(pa * pd - pb * pc) / Math.max(0.0001, pa * pa + pc * pc);
var sa:Float = pa / bone.skeleton.scaleX;
var sc:Float = pc / bone.skeleton.scaleY;
pb = -sc * s * bone.skeleton.scaleX;
pd = sa * s * bone.skeleton.scaleY;
rotationIK += Math.atan2(sc, sa) * MathUtils.radDeg;
var x:Float = targetX - p.worldX, y:Float = targetY - p.worldY;
var d:Float = pa * pd - pb * pc;
tx = (x * pd - y * pb) / d - bone.ax;
ty = (y * pa - x * pc) / d - bone.ay;
var sa:Float = pa / skeleton.scaleX;
var sc:Float = pc / skeleton.scaleY;
pb = -sc * s * skeleton.scaleX;
pd = sa * s * skeleton.scaleY;
rotationIK += MathUtils.atan2Deg(sc, sa);
switchDefault(); // Fall through.
default:
switchDefault();
}
rotationIK += Math.atan2(ty, tx) * MathUtils.radDeg;
if (bone.ascaleX < 0)
rotationIK += 180;
rotationIK += MathUtils.atan2Deg(ty, tx);
if (bone.scaleX < 0) rotationIK += 180;
if (rotationIK > 180)
rotationIK -= 360;
else if (rotationIK < -180)
else if (rotationIK < -180) //
rotationIK += 360;
var sx:Float = bone.ascaleX;
var sy:Float = bone.ascaleY;
bone.rotation += rotationIK * mix;
if (compress || stretch) {
switch (bone.inherit) {
case Inherit.noScale, Inherit.noScaleOrReflection:
tx = targetX - bone.worldX;
ty = targetY - bone.worldY;
}
var b:Float = bone.data.length * sx;
var b = bone.bone.data.length * bone.scaleX;
if (b > 0.0001) {
var dd:Float = tx * tx + ty * ty;
var dd = tx * tx + ty * ty;
if ((compress && dd < b * b) || (stretch && dd > b * b)) {
var s:Float = (Math.sqrt(dd) / b - 1) * alpha + 1;
sx *= s;
if (uniform) sy *= s;
var s = (Math.sqrt(dd) / b - 1) * mix + 1;
bone.scaleX *= s;
if (uniform) bone.scaleY *= s;
}
}
}
bone.updateWorldTransformWith(bone.ax, bone.ay, bone.arotation + rotationIK * alpha, sx, sy, bone.ashearX, bone.ashearY);
}
/** Applies 2 bone IK. The target is specified in the world coordinate system.
* @param child A direct descendant of the parent bone. */
static public function apply2(parent:Bone, child:Bone, targetX:Float, targetY:Float, bendDir:Int, stretch:Bool, uniform:Bool, softness:Float,
alpha:Float):Void {
static public function apply2(skeleton:Skeleton, parent:BonePose, child:BonePose, targetX:Float, targetY:Float, bendDir:Int,
stretch:Bool, uniform:Bool, softness:Float, mix:Float):Void {
if (parent == null) throw new SpineException("parent cannot be null.");
if (child == null) throw new SpineException("child cannot be null.");
if (parent.inherit != Inherit.normal || child.inherit != Inherit.normal) return;
var px:Float = parent.ax;
var py:Float = parent.ay;
var psx:Float = parent.ascaleX;
var sx:Float = psx;
var psy:Float = parent.ascaleY;
var sy:Float = psy;
var csx:Float = child.ascaleX;
var os1:Int;
var os2:Int;
var s2:Int;
parent.modifyLocal(skeleton);
child.modifyLocal(skeleton);
var px = parent.x, py = parent.y, psx = parent.scaleX, psy = parent.scaleY, csx = child.scaleX;
var os1 = 0, os2 = 0, s2 = 0;
if (psx < 0) {
psx = -psx;
os1 = 180;
@ -213,43 +178,30 @@ class IkConstraint implements Updatable {
if (csx < 0) {
csx = -csx;
os2 = 180;
} else {
} else
os2 = 0;
}
var cx:Float = child.ax;
var cy:Float;
var cwx:Float;
var cwy:Float;
var a:Float = parent.a;
var b:Float = parent.b;
var c:Float = parent.c;
var d:Float = parent.d;
var u:Bool = Math.abs(psx - psy) <= 0.0001;
var cwx = 0., cwy = 0., a = parent.a, b = parent.b, c = parent.c, d = parent.d;
var u = Math.abs(psx - psy) <= 0.0001;
if (!u || stretch) {
cy = 0;
cwx = a * cx + parent.worldX;
cwy = c * cx + parent.worldY;
child.y = 0;
cwx = a * child.x + parent.worldX;
cwy = c * child.x + parent.worldY;
} else {
cy = child.ay;
cwx = a * cx + b * cy + parent.worldX;
cwy = c * cx + d * cy + parent.worldY;
cwx = a * child.x + b * child.y + parent.worldX;
cwy = c * child.x + d * child.y + parent.worldY;
}
var pp:Bone = parent.parent;
var pp = parent.bone.parent.applied;
a = pp.a;
b = pp.b;
c = pp.c;
d = pp.d;
var id = a * d - b * c, x = cwx - pp.worldX, y = cwy - pp.worldY;
id = Math.abs(id) <= 0.0001 ? 0 : 1 / id;
var dx:Float = (x * d - y * b) * id - px,
dy:Float = (y * a - x * c) * id - py;
var l1:Float = Math.sqrt(dx * dx + dy * dy);
var l2:Float = child.data.length * csx;
var a1:Float = 0;
var a2:Float = 0;
var dx = (x * d - y * b) * id - px, dy = (y * a - x * c) * id - py;
var l1 = Math.sqrt(dx * dx + dy * dy), l2 = child.bone.data.length * csx, a1 = 0., a2 = 0.;
if (l1 < 0.0001) {
apply1(parent, targetX, targetY, false, stretch, false, alpha);
child.updateWorldTransformWith(cx, cy, 0, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
apply1(skeleton, parent, targetX, targetY, false, stretch, false, mix);
child.rotation = 0;
return;
}
x = targetX - pp.worldX;
@ -279,10 +231,9 @@ class IkConstraint implements Updatable {
} else if (cos > 1) {
cos = 1;
if (stretch) {
a = (Math.sqrt(dd) / (l1 + l2) - 1) * alpha + 1;
sx *= a;
if (uniform)
sy *= a;
a = (Math.sqrt(dd) / (l1 + l2) - 1) * mix + 1;
parent.scaleX *= a;
if (uniform) parent.scaleY *= a;
}
}
a2 = Math.acos(cos) * bendDir;
@ -352,23 +303,18 @@ class IkConstraint implements Updatable {
}
}
}
var os:Float = Math.atan2(cy, cx) * s2;
var rotation:Float = parent.arotation;
a1 = (a1 - os) * MathUtils.radDeg + os1 - rotation;
if (a1 > 180) {
var os:Float = Math.atan2(child.y, child.x) * s2;
a1 = (a1 - os) * MathUtils.radDeg + os1 - parent.rotation;
if (a1 > 180)
a1 -= 360;
} else if (a1 < -180) {
else if (a1 < -180) //
a1 += 360;
}
parent.updateWorldTransformWith(px, py, rotation + a1 * alpha, sx, sy, 0, 0);
rotation = child.arotation;
a2 = ((a2 + os) * MathUtils.radDeg - child.ashearX) * s2 + os2 - rotation;
if (a2 > 180) {
parent.rotation += a1 * mix;
a2 = ((a2 + os) * MathUtils.radDeg - child.shearX) * s2 + os2 - child.rotation;
if (a2 > 180)
a2 -= 360;
} else if (a2 < -180) {
else if (a2 < -180) //
a2 += 360;
}
child.updateWorldTransformWith(cx, cy, rotation + a2 * alpha, child.ascaleX, child.ascaleY, child.ashearX, child.ashearY);
child.rotation += a2 * mix;
}
}

View File

@ -30,33 +30,32 @@
package spine;
/** Stores the setup pose for a spine.IkConstraint.
*
*
* @see https://esotericsoftware.com/spine-ik-constraints IK constraints in the Spine User Guide */
class IkConstraintData extends ConstraintData {
class IkConstraintData extends ConstraintData<IkConstraint, IkConstraintPose> {
/** The bones that are constrained by this IK constraint. */
public var bones:Array<BoneData> = new Array<BoneData>();
public final bones:Array<BoneData> = new Array<BoneData>();
/** The bone that is the IK target. */
public var target:BoneData;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
*
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
public var mix:Float = 0;
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
public var bendDirection:Int = 0;
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
public var compress:Bool = false;
/** When true and the target is out of range, the parent bone is scaled to reach it.
*
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if softness is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
public var stretch:Bool = false;
/** When true and compress or stretch is used, the bone is scaled on both the X and Y axes. */
public var uniform:Bool = false;
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
* will not straighten completely until the target is this far out of range. */
public var softness:Float = 0;
public var target(default, set):BoneData;
/** When true and IkConstraintPose.compress or IkConstraintPose.stretch is used, the bone is scaled
* on both the X and Y axes. */
public var uniform = false;
public function new(name:String) {
super(name, 0, false);
super(name, new IkConstraintPose());
}
public function create (skeleton:Skeleton):IkConstraint {
return new IkConstraint(this, skeleton);
}
public function set_target (target:BoneData) {
if (target == null) throw new SpineException("target cannot be null.");
this.target = target;
return target;
}
}

View File

@ -0,0 +1,67 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores the current pose for an IK constraint. */
class IkConstraintPose implements Pose<IkConstraintPose> {
/** For two bone IK, controls the bend direction of the IK bones, either 1 or -1. */
public var bendDirection = 0;
/** For one bone IK, when true and the target is too close, the bone is scaled to reach it. */
public var compress:Bool = false;
/** When true and the target is out of range, the parent bone is scaled to reach it.
*
* For two bone IK: 1) the child bone's local Y translation is set to 0, 2) stretch is not applied if softness is
* > 0, and 3) if the parent bone has local nonuniform scale, stretch is not applied. */
public var stretch:Bool = false;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation.
*
* For two bone IK: if the parent bone has local nonuniform scale, the child bone's local Y translation is set to 0. */
public var mix = 0.;
/** For two bone IK, the target bone's distance from the maximum reach of the bones where rotation begins to slow. The bones
* will not straighten completely until the target is this far out of range. */
public var softness = 0.;
public function new () {
}
public function set (pose:IkConstraintPose) {
mix = pose.mix;
softness = pose.softness;
bendDirection = pose.bendDirection;
compress = pose.compress;
stretch = pose.stretch;
}
}

View File

@ -29,194 +29,140 @@
package spine;
import spine.Skeleton;
import spine.attachments.Attachment;
import spine.attachments.PathAttachment;
/** Stores the current pose for a path constraint. A path constraint adjusts the rotation, translation, and scale of the
* constrained bones so they follow a PathAttachment.
*
* @see https://esotericsoftware.com/spine-path-constraints Path constraints in the Spine User Guide */
class PathConstraint implements Updatable {
private static inline var NONE:Int = -1;
private static inline var BEFORE:Int = -2;
private static inline var AFTER:Int = -3;
private static inline var epsilon:Float = 0.00001;
class PathConstraint extends Constraint<PathConstraint, PathConstraintData, PathConstraintPose> {
private static inline var NONE = -1;
private static inline var BEFORE = -2;
private static inline var AFTER = -3;
private static inline var epsilon = 0.00001;
private var _data:PathConstraintData;
private var _bones:Array<Bone>;
/** The bones that will be modified by this path constraint. */
public final bones:Array<BonePose>;
/** The slot whose path attachment will be used to constrained the bones. */
public var target:Slot;
/** The position along the path. */
public var position:Float = 0;
/** The spacing between bones. */
public var spacing:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
public var mixRotate:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
public var mixX:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
public var mixY:Float = 0;
public var slot:Slot;
private var _spaces(default, never):Array<Float> = new Array<Float>();
private var _positions(default, never):Array<Float> = new Array<Float>();
private var _world(default, never):Array<Float> = new Array<Float>();
private var _curves(default, never):Array<Float> = new Array<Float>();
private var _lengths(default, never):Array<Float> = new Array<Float>();
private var _segments(default, never):Array<Float> = new Array<Float>();
private final spaces = new Array<Float>();
private final positions = new Array<Float>();
private final world = new Array<Float>();
private final curves = new Array<Float>();
private final lengths = new Array<Float>();
private final segments = new Array<Float>();
public var active:Bool = false;
public function new (data:PathConstraintData, skeleton:Skeleton) {
super(data, new PathConstraintPose(), new PathConstraintPose());
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
public function new(data:PathConstraintData, skeleton:Skeleton) {
if (data == null)
throw new SpineException("data cannot be null.");
if (skeleton == null)
throw new SpineException("skeleton cannot be null.");
_data = data;
bones = new Array<BonePose>();
for (boneData in data.bones)
bones.push(skeleton.bones[boneData.index].constrained);
_bones = new Array<Bone>();
for (boneData in data.bones) {
_bones.push(skeleton.findBone(boneData.name));
}
target = skeleton.findSlot(data.target.name);
position = data.position;
spacing = data.spacing;
mixRotate = data.mixRotate;
mixX = data.mixX;
mixY = data.mixY;
slot = skeleton.slots[data.slot.index];
}
public function isActive():Bool {
return active;
}
public function setToSetupPose () {
var data:PathConstraintData = _data;
position = data.position;
spacing = data.spacing;
mixRotate = data.mixRotate;
mixX = data.mixX;
mixY = data.mixY;
public function copy(skeleton:Skeleton) {
var copy = new PathConstraint(data, skeleton);
copy.pose.set(pose);
return copy;
}
/** Applies the constraint to the constrained bones. */
public function update(physics:Physics):Void {
var attachment:PathAttachment = cast(target.attachment, PathAttachment);
if (attachment == null)
return;
if (mixRotate == 0 && mixX == 0 && mixY == 0)
return;
public function update(skeleton:Skeleton, physics:Physics):Void {
var attachment = slot.applied.attachment;
if (!Std.isOfType(attachment, PathAttachment)) return;
var pathAttachment = cast(attachment, PathAttachment);
var data:PathConstraintData = _data;
var fTangents:Bool = data.rotateMode == RotateMode.tangent,
fScale:Bool = data.rotateMode == RotateMode.chainScale;
var boneCount:Int = _bones.length;
var spacesCount:Int = fTangents ? boneCount : boneCount + 1;
ArrayUtils.resize(_spaces, spacesCount, 0);
if (fScale) {
ArrayUtils.resize(_lengths, boneCount, 0);
}
var p = applied;
var mixRotate = p.mixRotate, mixX = p.mixX, mixY = p.mixY;
if (mixRotate == 0 && mixX == 0 && mixY == 0) return;
var bones:Array<Bone> = _bones;
var data = data;
var fTangents = data.rotateMode == RotateMode.tangent, fScale = data.rotateMode == RotateMode.chainScale;
var boneCount = bones.length, spacesCount = fTangents ? boneCount : boneCount + 1;
ArrayUtils.resize(spaces, spacesCount, 0);
if (fScale) ArrayUtils.resize(lengths, boneCount, 0);
var spacing = p.spacing;
var i:Int,
n:Int,
bone:Bone,
setupLength:Float,
x:Float,
y:Float,
length:Float;
var bones = bones;
switch (data.spacingMode) {
case SpacingMode.percent:
if (fScale) {
n = spacesCount - 1;
var n = spacesCount - 1;
for (i in 0...n) {
bone = bones[i];
setupLength = bone.data.length;
x = setupLength * bone.a;
y = setupLength * bone.c;
_lengths[i] = Math.sqrt(x * x + y * y);
var bone = bones[i];
var setupLength:Float = bone.bone.data.length;
var x = setupLength * bone.a, y = setupLength * bone.c;
lengths[i] = Math.sqrt(x * x + y * y);
}
}
for (i in 1...spacesCount) {
_spaces[i] = spacing;
}
for (i in 1...spacesCount) spaces[i] = spacing;
case SpacingMode.proportional:
var sum:Float = 0;
i = 0;
n = spacesCount - 1;
var sum = 0.;
var i = 0, n = spacesCount - 1;
while (i < n) {
bone = bones[i];
setupLength = bone.data.length;
var bone = bones[i];
var setupLength:Float = bone.bone.data.length;
if (setupLength < PathConstraint.epsilon) {
if (fScale)
_lengths[i] = 0;
_spaces[++i] = spacing;
if (fScale) lengths[i] = 0;
spaces[++i] = spacing;
} else {
x = setupLength * bone.a;
y = setupLength * bone.c;
length = Math.sqrt(x * x + y * y);
if (fScale)
_lengths[i] = length;
_spaces[++i] = length;
var x = setupLength * bone.a, y = setupLength * bone.c;
var length = Math.sqrt(x * x + y * y);
if (fScale) lengths[i] = length;
spaces[++i] = length;
sum += length;
}
}
if (sum > 0) {
sum = spacesCount / sum * spacing;
for (i in 1...spacesCount) {
_spaces[i] *= sum;
}
for (i in 1...spacesCount)
spaces[i] *= sum;
}
default:
var lengthSpacing:Bool = data.spacingMode == SpacingMode.length;
i = 0;
n = spacesCount - 1;
var lengthSpacing = data.spacingMode == SpacingMode.length;
var i = 0, n = spacesCount - 1;
while (i < n) {
bone = bones[i];
setupLength = bone.data.length;
var bone = bones[i];
var setupLength = bone.bone.data.length;
if (setupLength < PathConstraint.epsilon) {
if (fScale)
_lengths[i] = 0;
_spaces[++i] = spacing;
if (fScale) lengths[i] = 0;
spaces[++i] = spacing;
} else {
x = setupLength * bone.a;
y = setupLength * bone.c;
length = Math.sqrt(x * x + y * y);
if (fScale)
_lengths[i] = length;
_spaces[++i] = (lengthSpacing ? setupLength + spacing : spacing) * length / setupLength;
var x = setupLength * bone.a, y = setupLength * bone.c;
var length = Math.sqrt(x * x + y * y);
if (fScale) lengths[i] = length;
spaces[++i] = (lengthSpacing ? Math.max(0, setupLength + spacing) : spacing) * length / setupLength;
}
}
}
var positions:Array<Float> = computeWorldPositions(attachment, spacesCount, fTangents);
var boneX:Float = positions[0];
var boneY:Float = positions[1];
var offsetRotation:Float = data.offsetRotation;
var tip:Bool = false;
if (offsetRotation == 0) {
var positions = computeWorldPositions(skeleton, pathAttachment, spacesCount, fTangents);
var boneX = positions[0], boneY = positions[1], offsetRotation = data.offsetRotation;
var tip = false;
if (offsetRotation == 0)
tip = data.rotateMode == RotateMode.chain;
} else {
else {
tip = false;
var pa:Bone = target.bone;
offsetRotation *= pa.a * pa.d - pa.b * pa.c > 0 ? MathUtils.degRad : -MathUtils.degRad;
var bone = slot.bone.applied;
offsetRotation *= bone.a * bone.d - bone.b * bone.c > 0 ? MathUtils.degRad : -MathUtils.degRad;
}
i = 0;
var p:Int = 3;
var i = 0, ip = 3, u = skeleton._update;
while (i < boneCount) {
var bone:Bone = bones[i];
var bone = bones[i];
bone.worldX += (boneX - bone.worldX) * mixX;
bone.worldY += (boneY - bone.worldY) * mixY;
var x:Float = positions[p];
var y:Float = positions[p + 1];
var dx:Float = x - boneX;
var dy:Float = y - boneY;
var x = positions[ip], y = positions[ip + 1], dx = x - boneX, dy = y - boneY;
if (fScale) {
var length = _lengths[i];
if (length != 0) {
var s:Float = (Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
var length = lengths[i];
if (length >= epsilon) {
var s = (Math.sqrt(dx * dx + dy * dy) / length - 1) * mixRotate + 1;
bone.a *= s;
bone.c *= s;
}
@ -224,35 +170,26 @@ class PathConstraint implements Updatable {
boneX = x;
boneY = y;
if (mixRotate > 0) {
var a:Float = bone.a,
b:Float = bone.b,
c:Float = bone.c,
d:Float = bone.d,
r:Float,
cos:Float,
sin:Float;
if (fTangents) {
r = positions[p - 1];
} else if (_spaces[i + 1] == 0) {
r = positions[p + 2];
} else {
var a = bone.a, b = bone.b, c = bone.c, d = bone.d, r:Float, cos:Float, sin:Float;
if (fTangents)
r = positions[ip - 1];
else if (spaces[i + 1] < epsilon)
r = positions[ip + 2];
else
r = Math.atan2(dy, dx);
}
r -= Math.atan2(c, a);
if (tip) {
cos = Math.cos(r);
sin = Math.sin(r);
var length:Float = bone.data.length;
var length = bone.bone.data.length;
boneX += (length * (cos * a - sin * c) - dx) * mixRotate;
boneY += (length * (sin * a + cos * c) - dy) * mixRotate;
} else {
} else
r += offsetRotation;
}
if (r > Math.PI) {
if (r > Math.PI)
r -= (Math.PI * 2);
} else if (r < -Math.PI) {
else if (r < -Math.PI) //
r += (Math.PI * 2);
}
r *= mixRotate;
cos = Math.cos(r);
sin = Math.sin(r);
@ -261,64 +198,55 @@ class PathConstraint implements Updatable {
bone.c = sin * a + cos * c;
bone.d = sin * b + cos * d;
}
bone.updateAppliedTransform();
bone.modifyWorld(u);
i++;
p += 3;
ip += 3;
}
}
private function computeWorldPositions(path:PathAttachment, spacesCount:Int, tangents:Bool):Array<Float> {
var position:Float = this.position;
ArrayUtils.resize(_positions, spacesCount * 3 + 2, 0);
var out:Array<Float> = _positions, world:Array<Float>;
var closed:Bool = path.closed;
var verticesLength:Int = path.worldVerticesLength;
var curveCount:Int = Std.int(verticesLength / 6);
var prevCurve:Int = NONE;
var multiplier:Float, i:Int;
private function computeWorldPositions(skeleton:Skeleton, path:PathAttachment, spacesCount:Int, tangents:Bool):Array<Float> {
var position = applied.position;
ArrayUtils.resize(positions, spacesCount * 3 + 2, 0);
var out:Array<Float> = positions, world:Array<Float>;
var closed = path.closed;
var verticesLength = path.worldVerticesLength, curveCount = Std.int(verticesLength / 6), prevCurve = NONE;
if (!path.constantSpeed) {
var lengths:Array<Float> = path.lengths;
var lengths = path.lengths;
curveCount -= closed ? 1 : 2;
var pathLength:Float = lengths[curveCount];
if (data.positionMode == PositionMode.percent)
position *= pathLength;
var pathLength = lengths[curveCount];
if (data.positionMode == PositionMode.percent) position *= pathLength;
var multiplier: Float;
switch (data.spacingMode) {
case SpacingMode.percent:
multiplier = pathLength;
case SpacingMode.proportional:
multiplier = pathLength / spacesCount;
default:
multiplier = 1;
case SpacingMode.percent: multiplier = pathLength;
case SpacingMode.proportional: multiplier = pathLength / spacesCount;
default: multiplier = 1;
}
ArrayUtils.resize(_world, 8, 0);
world = _world;
var i:Int = 0;
var o:Int = 0;
var curve:Int = 0;
ArrayUtils.resize(world, 8, 0);
var i = 0, o = 0, curve = 0;
while (i < spacesCount) {
var space:Float = _spaces[i] * multiplier;
var space = spaces[i] * multiplier;
position += space;
var p:Float = position;
var p = position;
if (closed) {
p %= pathLength;
if (p < 0)
p += pathLength;
if (p < 0) p += pathLength;
curve = 0;
} else if (p < 0) {
if (prevCurve != BEFORE) {
prevCurve = BEFORE;
path.computeWorldVertices(target, 2, 4, world, 0, 2);
path.computeWorldVertices(skeleton, slot, 2, 4, world, 0, 2);
}
addBeforePosition(p, world, 0, out, o);
continue;
} else if (p > pathLength) {
if (prevCurve != AFTER) {
prevCurve = AFTER;
path.computeWorldVertices(target, verticesLength - 6, 4, world, 0, 2);
path.computeWorldVertices(skeleton, slot, verticesLength - 6, 4, world, 0, 2);
}
addAfterPosition(p - pathLength, world, 0, out, o);
continue;
@ -326,15 +254,15 @@ class PathConstraint implements Updatable {
// Determine curve containing position.
while (true) {
var length:Float = lengths[curve];
var length = lengths[curve];
if (p > length) {
curve++;
continue;
}
if (curve == 0) {
if (curve == 0)
p /= length;
} else {
var prev:Float = lengths[curve - 1];
else {
var prev = lengths[curve - 1];
p = (p - prev) / (length - prev);
}
break;
@ -342,14 +270,14 @@ class PathConstraint implements Updatable {
if (curve != prevCurve) {
prevCurve = curve;
if (closed && curve == curveCount) {
path.computeWorldVertices(target, verticesLength - 4, 4, world, 0, 2);
path.computeWorldVertices(target, 0, 4, world, 4, 2);
path.computeWorldVertices(skeleton, slot, verticesLength - 4, 4, world, 0, 2);
path.computeWorldVertices(skeleton, slot, 0, 4, world, 4, 2);
} else {
path.computeWorldVertices(target, curve * 6 + 2, 8, world, 0, 2);
path.computeWorldVertices(skeleton, slot, curve * 6 + 2, 8, world, 0, 2);
}
}
addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o, tangents || (i > 0 && space == 0));
addCurvePosition(p, world[0], world[1], world[2], world[3], world[4], world[5], world[6], world[7], out, o,
tangents || (i > 0 && space == 0));
i++;
o += 3;
}
@ -359,35 +287,25 @@ class PathConstraint implements Updatable {
// World vertices.
if (closed) {
verticesLength += 2;
ArrayUtils.resize(_world, verticesLength, 0);
world = _world;
path.computeWorldVertices(target, 2, verticesLength - 4, world, 0, 2);
path.computeWorldVertices(target, 0, 2, world, verticesLength - 4, 2);
ArrayUtils.resize(world, verticesLength, 0);
path.computeWorldVertices(skeleton, slot, 2, verticesLength - 4, world, 0, 2);
path.computeWorldVertices(skeleton, slot, 0, 2, world, verticesLength - 4, 2);
world[verticesLength - 2] = world[0];
world[verticesLength - 1] = world[1];
} else {
curveCount--;
verticesLength -= 4;
ArrayUtils.resize(_world, verticesLength, 0);
world = _world;
path.computeWorldVertices(target, 2, verticesLength, world, 0, 2);
ArrayUtils.resize(world, verticesLength, 0);
path.computeWorldVertices(skeleton, slot, 2, verticesLength, world, 0, 2);
}
// Curve lengths.
ArrayUtils.resize(_curves, curveCount, 0);
var curves:Array<Float> = _curves;
ArrayUtils.resize(curves, curveCount, 0);
var curves:Array<Float> = curves;
var pathLength:Float = 0;
var x1:Float = world[0],
y1:Float = world[1],
cx1:Float = 0,
cy1:Float = 0,
cx2:Float = 0,
cy2:Float = 0,
x2:Float = 0,
y2:Float = 0;
var x1 = world[0], y1 = world[1], cx1 = 0., cy1 = 0., cx2 = 0., cy2 = 0., x2 = 0., y2 = 0.;
var tmpx:Float, tmpy:Float, dddfx:Float, dddfy:Float, ddfx:Float, ddfy:Float, dfx:Float, dfy:Float;
var i:Int = 0;
var w:Int = 2;
var i = 0, w = 2;
while (i < curveCount) {
cx1 = world[w];
cy1 = world[w + 1];
@ -423,33 +341,28 @@ class PathConstraint implements Updatable {
w += 6;
}
if (data.positionMode == PositionMode.percent)
position *= pathLength;
if (data.positionMode == PositionMode.percent) position *= pathLength;
var multiplier:Float;
switch (data.spacingMode) {
case SpacingMode.percent:
multiplier = pathLength;
case SpacingMode.proportional:
multiplier = pathLength / spacesCount;
default:
multiplier = 1;
case SpacingMode.percent: multiplier = pathLength;
case SpacingMode.proportional: multiplier = pathLength / spacesCount;
default: multiplier = 1;
}
var segments:Array<Float> = _segments;
var curveLength:Float = 0;
var segment:Int;
i = 0;
var o:Int = 0;
var segment:Int = 0;
var segments = segments;
var curveLength = 0.;
var i = 0, o = 0, curve = 0, segment = 0;
while (i < spacesCount) {
var space = _spaces[i] * multiplier;
var space = spaces[i] * multiplier;
position += space;
var p = position;
if (closed) {
p %= pathLength;
if (p < 0)
p += pathLength;
if (p < 0) p += pathLength;
curve = 0;
segment = 0;
} else if (p < 0) {
addBeforePosition(p, world, 0, out, o);
i++;
@ -528,9 +441,9 @@ class PathConstraint implements Updatable {
segment++;
continue;
}
if (segment == 0) {
if (segment == 0)
p /= length;
} else {
else {
var prev = segments[segment - 1];
p = segment + (p - prev) / (length - prev);
}
@ -596,21 +509,51 @@ class PathConstraint implements Updatable {
}
}
/** The bones that will be modified by this path constraint. */
public var bones(get, never):Array<Bone>;
private function get_bones():Array<Bone> {
return _bones;
public function sort (skeleton:Skeleton) {
var slotIndex = slot.data.index;
var slotBone = slot.bone;
if (skeleton.skin != null) sortPathSlot(skeleton, skeleton.skin, slotIndex, slotBone);
if (skeleton.data.defaultSkin != null && skeleton.data.defaultSkin != skeleton.skin)
sortPathSlot(skeleton, skeleton.data.defaultSkin, slotIndex, slotBone);
sortPath(skeleton, slot.pose.attachment, slotBone);
var boneCount = bones.length;
for (i in 0...boneCount) {
var bone = bones[i].bone;
skeleton.sortBone(bone);
skeleton.constrained(bone);
}
skeleton._updateCache.push(this);
for (i in 0...boneCount)
skeleton.sortReset(bones[i].bone.children);
for (i in 0...boneCount)
bones[i].bone.sorted = true;
}
/** The path constraint's setup pose data. */
public var data(get, never):PathConstraintData;
private function get_data():PathConstraintData {
return _data;
public function sortPathSlot (skeleton:Skeleton, skin:Skin, slotIndex:Int, slotBone:Bone) {
var entries = skin.getAttachments();
for (entry in entries) {
if (entry.slotIndex == slotIndex) sortPath(skeleton, entry.attachment, slotBone);
}
}
public function toString():String {
return _data.name != null ? _data.name : "PathConstraint?";
private function sortPath (skeleton:Skeleton, attachment:Attachment, slotBone:Bone) {
if (!(Std.isOfType(attachment, PathAttachment))) return;
var pathBones = cast(attachment, PathAttachment).bones;
if (pathBones == null)
skeleton.sortBone(slotBone);
else {
var bones = skeleton.bones;
var i = 0, n = pathBones.length;
while (i < n) {
var nn = pathBones[i++];
nn += i;
while (i < nn)
skeleton.sortBone(bones[pathBones[i++]]);
}
}
}
override public function isSourceActive (): Bool {
return slot.bone.active;
}
}

View File

@ -30,41 +30,56 @@
package spine;
/** Stores the setup pose for a spine.PathConstraint.
*
*
* @see https://esotericsoftware.com/spine-path-constraints Path constraints in the Spine User Guide */
class PathConstraintData extends ConstraintData {
class PathConstraintData extends ConstraintData<PathConstraint, PathConstraintPose> {
/** The bones that will be modified by this path constraint. */
private var _bones:Array<BoneData> = new Array<BoneData>();
public final bones:Array<BoneData> = new Array<BoneData>();
/** The slot whose path attachment will be used to constrain the bones. */
public var target:SlotData;
public var slot(default, set):SlotData;
/** The mode for positioning the first bone on the path. */
public var positionMode:PositionMode = PositionMode.fixed;
public var positionMode(default, set):PositionMode = PositionMode.fixed;
/** The mode for positioning the bones after the first bone on the path. */
public var spacingMode:SpacingMode = SpacingMode.fixed;
public var spacingMode(default, set):SpacingMode = SpacingMode.fixed;
/** The mode for adjusting the rotation of the bones. */
public var rotateMode:RotateMode = RotateMode.chain;
public var rotateMode(default, set):RotateMode = RotateMode.chain;
/** An offset added to the constrained bone rotation. */
public var offsetRotation:Float = 0;
/** The position along the path. */
public var position:Float = 0;
/** The spacing between bones. */
public var spacing:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
public var mixRotate:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
public var mixX:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
public var mixY:Float = 0;
public function new(name:String) {
super(name, 0, false);
public function new (name:String) {
super(name, new PathConstraintPose());
}
/** The bones that will be modified by this path constraint. */
public var bones(get, never):Array<BoneData>;
public function create (skeleton:Skeleton) {
return new PathConstraint(this, skeleton);
}
private function get_bones():Array<BoneData> {
return _bones;
public function set_slot (slot:SlotData):SlotData {
if (slot == null) throw new SpineException("slot cannot be null.");
this.slot = slot;
return slot;
}
public function set_positionMode (positionMode:PositionMode):PositionMode {
if (positionMode == null) throw new SpineException("positionMode cannot be null.");
this.positionMode = positionMode;
return positionMode;
}
public function set_spacingMode (spacingMode:SpacingMode):SpacingMode {
if (spacingMode == null) throw new SpineException("spacingMode cannot be null.");
this.spacingMode = spacingMode;
return spacingMode;
}
public function set_rotateMode (rotateMode:RotateMode):RotateMode {
if (rotateMode == null) throw new SpineException("rotateMode cannot be null.");
this.rotateMode = rotateMode;
return rotateMode;
}
}

View File

@ -0,0 +1,59 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
class PathConstraintPose implements Pose<PathConstraintPose> {
/** The position along the path. */
public var position = 0.;
/** The spacing between bones. */
public var spacing = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
public var mixRotate = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
public var mixX = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
public var mixY = 0.;
public function new () {
}
public function set(pose:PathConstraintPose) {
position = pose.position;
spacing = pose.spacing;
mixRotate = pose.mixRotate;
mixX = pose.mixX;
mixY = pose.mixY;
}
}

View File

@ -30,277 +30,72 @@
package spine;
/** Stores the current pose for a physics constraint. A physics constraint applies physics to bones.
*
*
*
*
* @see https://esotericsoftware.com/spine-physics-constraints Physics constraints in the Spine User Guide
*/
class PhysicsConstraint implements Updatable {
private var _data:PhysicsConstraintData;
private var _bone:Bone = null;
class PhysicsConstraint extends Constraint<PhysicsConstraint, PhysicsConstraintData, PhysicsConstraintPose> {
public var inertia:Float = 0;
public var strength:Float = 0;
public var damping:Float = 0;
public var massInverse:Float = 0;
public var wind:Float = 0;
public var gravity:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */
public var mix:Float = 0;
/** The bone constrained by this physics constraint. */
public var bone:BonePose = null;
private var _reset:Bool = true;
public var _reset = true;
public var ux:Float = 0;
public var uy:Float = 0;
public var cx:Float = 0;
public var cy:Float = 0;
public var tx:Float = 0;
public var ty:Float = 0;
public var xOffset:Float = 0;
public var xVelocity:Float = 0;
public var yOffset:Float = 0;
public var yVelocity:Float = 0;
public var rotateOffset:Float = 0;
public var rotateVelocity:Float = 0;
public var scaleOffset:Float = 0;
public var scaleVelocity:Float = 0;
public var active:Bool = false;
private var _skeleton:Skeleton;
public var ux = 0.;
public var uy = 0.;
public var cx = 0.;
public var cy = 0.;
public var tx = 0.;
public var ty = 0.;
public var xOffset = 0.;
public var xLag = 0.;
public var xVelocity = 0.;
public var yOffset = 0.;
public var yLag = 0.;
public var yVelocity = 0.;
public var rotateOffset = 0.;
public var rotateLag = 0.;
public var rotateVelocity = 0.;
public var scaleOffset = 0.;
public var scaleLag = 0.;
public var scaleVelocity = 0.;
public var remaining:Float = 0;
public var lastTime:Float = 0;
public function new(data: PhysicsConstraintData, skeleton: Skeleton) {
_data = data;
_skeleton = skeleton;
super(data, new PhysicsConstraintPose(), new PhysicsConstraintPose());
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
_bone = skeleton.bones[data.bone.index];
inertia = data.inertia;
strength = data.strength;
damping = data.damping;
massInverse = data.massInverse;
wind = data.wind;
gravity = data.gravity;
mix = data.mix;
bone = skeleton.bones[data.bone.index].constrained;
}
public function reset () {
public function copy(skeleton:Skeleton) {
var copy = new PhysicsConstraint(data, skeleton);
copy.pose.set(pose);
return copy;
}
public function reset (skeleton:Skeleton) {
remaining = 0;
lastTime = skeleton.time;
_reset = true;
xOffset = 0;
xLag = 0;
xVelocity = 0;
yOffset = 0;
yLag = 0;
yVelocity = 0;
rotateOffset = 0;
rotateLag = 0;
rotateVelocity = 0;
scaleOffset = 0;
scaleLag = 0;
scaleVelocity = 0;
}
public function setToSetupPose () {
var data:PhysicsConstraintData = _data;
inertia = data.inertia;
strength = data.strength;
damping = data.damping;
massInverse = data.massInverse;
wind = data.wind;
gravity = data.gravity;
mix = data.mix;
}
public function isActive():Bool {
return active;
}
/** Applies the constraint to the constrained bones. */
public function update(physics:Physics):Void {
var mix:Float = this.mix;
if (mix == 0) return;
var x:Bool = _data.x > 0, y:Bool = _data.y > 0,
rotateOrShearX:Bool = _data.rotate > 0 || _data.shearX > 0,
scaleX:Bool = _data.scaleX > 0;
var bone:Bone = _bone;
var l:Float = bone.data.length;
switch (physics) {
case Physics.none:
return;
case Physics.reset, Physics.update:
if (physics == Physics.reset) reset();
var delta:Float = Math.max(skeleton.time - lastTime, 0);
remaining += delta;
lastTime = _skeleton.time;
var bx:Float = bone.worldX, by:Float = bone.worldY;
if (_reset) {
_reset = false;
ux = bx;
uy = by;
} else {
var a:Float = remaining,
i:Float = inertia,
t:Float = _data.step,
f:Float = skeleton.data.referenceScale,
d:Float = -1;
var qx:Float = _data.limit * delta,
qy:Float = qx * Math.abs(skeleton.scaleY);
qx *= Math.abs(skeleton.scaleX);
if (x || y) {
if (x) {
var u:Float = (ux - bx) * i;
xOffset += u > qx ? qx : u < -qx ? -qx : u;
ux = bx;
}
if (y) {
var u:Float = (uy - by) * i;
yOffset += u > qy ? qy : u < -qy ? -qy : u;
uy = by;
}
if (a >= t) {
d = Math.pow(damping, 60 * t);
var m:Float = massInverse * t,
e:Float = strength,
w:Float = wind * f * skeleton.scaleX,
g:Float = gravity * f * skeleton.scaleY;
do {
if (x) {
xVelocity += (w - xOffset * e) * m;
xOffset += xVelocity * t;
xVelocity *= d;
}
if (y) {
yVelocity -= (g + yOffset * e) * m;
yOffset += yVelocity * t;
yVelocity *= d;
}
a -= t;
} while (a >= t);
}
if (x) bone.worldX += xOffset * mix * data.x;
if (y) bone.worldY += yOffset * mix * data.y;
}
if (rotateOrShearX || scaleX) {
var ca:Float = Math.atan2(bone.c, bone.a),
c:Float = 0,
s:Float = 0,
mr:Float = 0;
var dx:Float = cx - bone.worldX,
dy:Float = cy - bone.worldY;
if (dx > qx)
dx = qx;
else if (dx < -qx) //
dx = -qx;
if (dy > qy)
dy = qy;
else if (dy < -qy) //
dy = -qy;
if (rotateOrShearX) {
mr = (_data.rotate + _data.shearX) * mix;
var r:Float = Math.atan2(dy + ty, dx + tx) - ca - rotateOffset * mr;
rotateOffset += (r - Math.ceil(r * MathUtils.invPI2 - 0.5) * MathUtils.PI2) * i;
r = rotateOffset * mr + ca;
c = Math.cos(r);
s = Math.sin(r);
if (scaleX) {
r = l * bone.worldScaleX;
if (r > 0) scaleOffset += (dx * c + dy * s) * i / r;
}
} else {
c = Math.cos(ca);
s = Math.sin(ca);
var r:Float = l * bone.worldScaleX;
if (r > 0) scaleOffset += (dx * c + dy * s) * i / r;
}
a = remaining;
if (a >= t) {
if (d == -1) d = Math.pow(damping, 60 * t);
var m:Float = massInverse * t,
e:Float = strength,
w:Float = wind,
g:Float = (Bone.yDown ? -gravity : gravity),
h:Float = l / f;
while (true) {
a -= t;
if (scaleX) {
scaleVelocity += (w * c - g * s - scaleOffset * e) * m;
scaleOffset += scaleVelocity * t;
scaleVelocity *= d;
}
if (rotateOrShearX) {
rotateVelocity -= ((w * s + g * c) * h + rotateOffset * e) * m;
rotateOffset += rotateVelocity * t;
rotateVelocity *= d;
if (a < t) break;
var r:Float = rotateOffset * mr + ca;
c = Math.cos(r);
s = Math.sin(r);
} else if (a < t) //
break;
}
}
}
remaining = a;
}
cx = bone.worldX;
cy = bone.worldY;
case Physics.pose:
if (x) bone.worldX += xOffset * mix * data.x;
if (y) bone.worldY += yOffset * mix * data.y;
}
if (rotateOrShearX) {
var o:Float = rotateOffset * mix,
s:Float = 0,
c:Float = 0,
a:Float = 0;
if (_data.shearX > 0) {
var r:Float = 0;
if (_data.rotate > 0) {
r = o * _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 * _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 *= _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) {
var s:Float = 1 + scaleOffset * mix * data.scaleX;
bone.a *= s;
bone.c *= s;
}
if (physics != Physics.pose) {
tx = l * bone.a;
ty = l * bone.c;
}
bone.updateAppliedTransform();
}
/** Translates the physics constraint so next update(Physics) forces are applied as if the bone moved an additional
* amount in world space. */
public function translate (x:Float, y:Float):Void {
public function translate (x:Float, y:Float):Void {
ux -= x;
uy -= y;
cx -= x;
@ -315,26 +110,199 @@ class PhysicsConstraint implements Updatable {
translate(dx * cos - dy * sin - dx, dx * sin + dy * cos - dy);
}
/** The bone constrained by this physics constraint. */
public var bone(get, never):Bone;
/** Applies the constraint to the constrained bones. */
public function update(skeleton:Skeleton, physics:Physics):Void {
var p = applied;
var mix = p.mix;
if (mix == 0) return;
private function get_bone():Bone {
if (_bone == null)
throw new SpineException("Bone not set.")
else return _bone;
var x = data.x > 0, y = data.y > 0, rotateOrShearX = data.rotate > 0 || data.shearX > 0, scaleX = data.scaleX > 0;
var l = bone.bone.data.length, t = data.step, z = 0.;
switch (physics) {
case Physics.none:
return;
case Physics.reset, Physics.update:
if (physics == Physics.reset) reset(skeleton);
var delta = Math.max(skeleton.time - lastTime, 0), aa = remaining;
remaining += delta;
lastTime = skeleton.time;
var bx = bone.worldX, by = bone.worldY;
if (_reset) {
_reset = false;
ux = bx;
uy = by;
} else {
var a = remaining, i = p.inertia, f = skeleton.data.referenceScale, d = -1., m = 0., e = 0., qx = data.limit * delta,
qy = qx * Math.abs(skeleton.scaleY);
qx *= Math.abs(skeleton.scaleX);
if (x || y) {
if (x) {
var u = (ux - bx) * i;
xOffset += u > qx ? qx : u < -qx ? -qx : u;
ux = bx;
}
if (y) {
var u = (uy - by) * i;
yOffset += u > qy ? qy : u < -qy ? -qy : u;
uy = by;
}
if (a >= t) {
var xs = xOffset, ys = yOffset;
d = Math.pow(p.damping, 60 * t);
m = t * p.massInverse;
e = p.strength;
var w = f * p.wind * skeleton.scaleX, g = f * p.gravity * skeleton.scaleY,
ax = w * skeleton.windX + g * skeleton.gravityX, ay = w * skeleton.windY + g * skeleton.gravityY;
do {
if (x) {
xVelocity += (ax - xOffset * e) * m;
xOffset += xVelocity * t;
xVelocity *= d;
}
if (y) {
yVelocity -= (ay + yOffset * e) * m;
yOffset += yVelocity * t;
yVelocity *= d;
}
a -= t;
} while (a >= t);
xLag = xOffset - xs;
yLag = yOffset - ys;
}
z = Math.max(0, 1 - a / t);
if (x) bone.worldX += xOffset * mix * this.data.x;
if (y) bone.worldY += yOffset * mix * this.data.y;
}
if (rotateOrShearX || scaleX) {
var ca = Math.atan2(bone.c, bone.a), c = 0., s = 0., mr = 0., dx = cx - bone.worldX, dy = cy - bone.worldY;
if (dx > qx)
dx = qx;
else if (dx < -qx) //
dx = -qx;
if (dy > qy)
dy = qy;
else if (dy < -qy) //
dy = -qy;
a = remaining;
if (rotateOrShearX) {
mr = (data.rotate + data.shearX) * mix;
z = rotateLag * Math.max(0, 1 - aa / t);
var r = Math.atan2(dy + ty, dx + tx) - ca - (rotateOffset - z) * mr;
rotateOffset += (r - Math.ceil(r * MathUtils.invPI2 - 0.5) * MathUtils.PI2) * i;
r = (rotateOffset - z) * mr + ca;
c = Math.cos(r);
s = Math.sin(r);
if (scaleX) {
r = l * bone.worldScaleX;
if (r > 0) scaleOffset += (dx * c + dy * s) * i / r;
}
} else {
c = Math.cos(ca);
s = Math.sin(ca);
var r = l * bone.worldScaleX - scaleLag * Math.max(0, 1 - aa / t);
if (r > 0) scaleOffset += (dx * c + dy * s) * i / r;
}
a = remaining;
if (a >= t) {
if (d == -1) {
d = Math.pow(p.damping, 60 * t);
m = t * p.massInverse;
e = p.strength;
}
var g = Bone.yDown ? -p.gravity : p.gravity;
var rs = rotateOffset, ss = scaleLag, h = l / f,
ax = p.wind * skeleton.windX + g * skeleton.gravityX,
ay = p.wind * skeleton.windY + g * skeleton.gravityY;
while (true) {
a -= t;
if (scaleX) {
scaleVelocity += (ax * c - ay * s - scaleOffset * e) * m;
scaleOffset += scaleVelocity * t;
scaleVelocity *= d;
}
if (rotateOrShearX) {
rotateVelocity -= ((ax * s + ay * c) * h + rotateOffset * e) * m;
rotateOffset += rotateVelocity * t;
rotateVelocity *= d;
if (a < t) break;
var r:Float = rotateOffset * mr + ca;
c = Math.cos(r);
s = Math.sin(r);
} else if (a < t) //
break;
}
rotateLag = rotateOffset - rs;
scaleLag = scaleOffset - ss;
}
z = Math.max(0, 1 - a / t);
}
remaining = a;
}
cx = bone.worldX;
cy = bone.worldY;
case Physics.pose:
z = Math.max(0, 1 - remaining / t);
if (x) bone.worldX += (xOffset - xLag * z) * mix * data.x;
if (y) bone.worldY += (yOffset - yLag * z) * mix * data.y;
}
if (rotateOrShearX) {
var o:Float = (rotateOffset - rotateLag * z) * mix,
s:Float = 0,
c:Float = 0,
a:Float = 0;
if (data.shearX > 0) {
var r:Float = 0;
if (data.rotate > 0) {
r = o * 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 * 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 *= 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) {
var s:Float = 1 + (scaleOffset - scaleLag * z) * mix * this.data.scaleX;
bone.a *= s;
bone.c *= s;
}
if (physics != Physics.pose) {
tx = l * bone.a;
ty = l * bone.c;
}
bone.modifyWorld(skeleton._update);
}
/** The physics constraint's setup pose data. */
public var data(get, never):PhysicsConstraintData;
private function get_data():PhysicsConstraintData {
return _data;
public function sort (skeleton: Skeleton) {
var bone = bone.bone;
skeleton.sortBone(bone);
skeleton._updateCache.push(this);
skeleton.sortReset(bone.children);
skeleton.constrained(bone);
}
public var skeleton(get, never):Skeleton;
private function get_skeleton():Skeleton {
return _skeleton;
override public function isSourceActive () {
return bone.bone.active;
}
}

View File

@ -30,36 +30,42 @@
package spine;
/** Stores the setup pose for a PhysicsConstraint.
*
*
* @see https://esotericsoftware.com/spine-physics-constraints Physics constraints in the Spine User Guide */
class PhysicsConstraintData extends ConstraintData {
class PhysicsConstraintData extends ConstraintData<PhysicsConstraint, PhysicsConstraintPose> {
/** The bone constrained by this physics constraint. */
public var bone:BoneData;
public var x:Float = 0;
public var y:Float = 0;
public var rotate:Float = 0;
public var scaleX:Float = 0;
public var shearX:Float = 0;
public var limit:Float = 0;
public var step:Float = 0;
public var inertia:Float = 0;
public var strength:Float = 0;
public var damping:Float = 0;
public var massInverse:Float = 0;
public var wind:Float = 0;
public var gravity:Float = 0;
public var x = 0.;
public var y = 0.;
public var rotate = 0.;
public var scaleX = 0.;
public var shearX = 0.;
public var limit = 0.;
public var step = 0.;
public var inertia = 0.;
public var strength = 0.;
public var damping = 0.;
public var massInverse = 0.;
public var wind = 0.;
public var gravity = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */
public var mix:Float = 0;
public var inertiaGlobal:Bool = false;
public var strengthGlobal:Bool = false;
public var dampingGlobal:Bool = false;
public var massGlobal:Bool = false;
public var windGlobal:Bool = false;
public var gravityGlobal:Bool = false;
public var mixGlobal:Bool = false;
public var mix = 0.;
public var inertiaGlobal = false;
public var strengthGlobal = false;
public var dampingGlobal = false;
public var massGlobal = false;
public var windGlobal = false;
public var gravityGlobal = false;
public var mixGlobal = false;
public function new(name:String) {
super(name, 0, false);
super(name, new PhysicsConstraintPose());
}
public function create (skeleton:Skeleton) {
return new PhysicsConstraint(this, skeleton);
}
}

View File

@ -0,0 +1,56 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores a pose for a physics constraint. */
class PhysicsConstraintPose implements Pose<PhysicsConstraintPose> {
public var inertia = 0.;
public var strength = 0.;
public var damping = 0.;
public var massInverse = 0.;
public var wind = 0.;
public var gravity = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained poses. */
public var mix = 0.;
public function new () {
}
public function set (pose:PhysicsConstraintPose) {
inertia = pose.inertia;
strength = pose.strength;
damping = pose.damping;
massInverse = pose.massInverse;
wind = pose.wind;
gravity = pose.gravity;
mix = pose.mix;
}
}

View File

@ -0,0 +1,34 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
interface Pose<P> {
public function set (pose:P):Void;
}

View File

@ -0,0 +1,59 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
abstract class Posed< //
D:PosedData<P>, //
P:Pose<Any>, //
A:P> {
/** The constraint's setup pose data. */
public final data:D;
public final pose:P;
public final constrained:A;
public var applied:A;
public function new (data:D, pose:P, constrained:A) {
if (data == null) throw new SpineException("data cannot be null.");
this.data = data;
this.pose = pose;
this.constrained = constrained;
applied = cast pose;
}
public function setupPose ():Void {
pose.set(data.setup);
}
public function toString ():String {
return data.name;
}
}

View File

@ -0,0 +1,55 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
abstract class PosedActive< //
D:PosedData<P>, //
P:Pose<Any>, //
A:P> //
extends Posed<D, P, A> {
public var active:Bool;
public function new (data:D, pose:P, constrained:A) {
super(data, pose, constrained);
setupPose();
}
/** Returns false when this constraint won't be updated by
* Skeleton.updateWorldTransform(Physics) because a skin is required and the
* Skeleton.getSkin() (active skin) does not contain this item.
* @see Skin.getBones()
* @see Skin.getConstraints()
* @see PosedData.getSkinRequired()
* @see Skeleton.updateCache() */
public function isActive ():Bool {
return active;
}
}

View File

@ -0,0 +1,54 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** The base class for all constrained datas. */
abstract class PosedData<P:Pose<Any>> {
/** The constraint's name, which is unique across all constraints in the skeleton of the same type. */
public var name:String;
public final setup:P;
/** When true, `Skeleton.updateWorldTransform(Physics)` only updates this constraint if the `Skeleton.getSkin()`
* contains this constraint.
*
* See `Skin.getConstraints()`. */
public var skinRequired:Bool;
public function new (name:String, setup:P) {
if (name == null) throw new SpineException("name cannot be null.");
this.name = name;
this.setup = setup;
}
public function toString ():String {
return name;
}
}

View File

@ -59,7 +59,7 @@ class Sequence {
return copy;
}
public function apply(slot:Slot, attachment:HasTextureRegion) {
public function apply(slot:SlotPose, attachment:HasTextureRegion) {
var index:Int = slot.sequenceIndex;
if (index == -1)
index = this.setupIndex;

View File

@ -43,69 +43,84 @@ import spine.attachments.RegionAttachment;
*/
class Skeleton {
private static var quadTriangles:Array<Int> = [0, 1, 2, 2, 3, 0];
private var _data:SkeletonData;
/** The skeleton's setup pose data. */
public final data:SkeletonData;
/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
public var bones:Array<Bone>;
public final bones:Array<Bone>;
/** The skeleton's slots. */
public var slots:Array<Slot>; // Setup pose draw order.
public final slots:Array<Slot>; // Setup pose draw order.
/** The skeleton's slots in the order they should be drawn. The returned array may be modified to change the draw order. */
public var drawOrder:Array<Slot>;
/** The skeleton's IK constraints. */
public var ikConstraints:Array<IkConstraint>;
/** The skeleton's transform constraints. */
public var transformConstraints:Array<TransformConstraint>;
/** The skeleton's path constraints. */
public var pathConstraints:Array<PathConstraint>;
/** The skeleton's physics constraints. */
public var physicsConstraints:Array<PhysicsConstraint>;
private var _updateCache:Array<Updatable> = new Array<Updatable>();
private var _skin:Skin;
/** The skeleton's constraints. */
public final constraints:Array<Constraint<Dynamic, Dynamic, Dynamic>>;
/** The skeleton's physics constraints. */
public final physics:Array<PhysicsConstraint>;
/** The list of bones and constraints, sorted in the order they should be updated, as computed by Skeleton.updateCache(). */
public final _updateCache = new Array<Dynamic>();
private final resetCache = new Array<Posed<Dynamic, Dynamic, Dynamic>> ();
/** The skeleton's current skin. */
public var skin(default, set):Skin = null;
/** The color to tint all the skeleton's attachments. */
public var color:Color = new Color(1, 1, 1, 1);
/** Scales the entire skeleton on the X axis.
*
* Bones that do not inherit scale are still affected by this property. */
public var scaleX:Float = 1;
/** Scales the entire skeleton on the Y axis.
*
* Bones that do not inherit scale are still affected by this property. */
public var scaleY(get, default):Float = 1;
function get_scaleY() {
return Bone.yDown ? -scaleY : scaleY;
}
public final color:Color;
/** Sets the skeleton X position, which is added to the root bone worldX position.
*
* Bones that do not inherit translation are still affected by this property. */
public var x:Float = 0;
/** Sets the skeleton Y position, which is added to the root bone worldY position.
*
* Bones that do not inherit translation are still affected by this property. */
public var y:Float = 0;
/** Scales the entire skeleton on the X axis.
*
* Bones that do not inherit scale are still affected by this property. */
public var scaleX:Float = 1;
/** Scales the entire skeleton on the Y axis.
*
* Bones that do not inherit scale are still affected by this property. */
public var scaleY(get, default):Float = 1;
function get_scaleY() {
return Bone.yDown ? -scaleY : scaleY;
}
/** Returns the skeleton's time. This is used for time-based manipulations, such as spine.PhysicsConstraint.
*
* See Skeleton.update(). */
public var time:Float = 0;
public var windX:Float = 1;
public var windY:Float = 0;
public var gravityX:Float = 0;
public var gravityY:Float = 1;
public var _update = 0;
/** Creates a new skeleton with the specified skeleton data. */
public function new(data:SkeletonData) {
if (data == null) {
throw new SpineException("data cannot be null.");
}
_data = data;
if (data == null) throw new SpineException("data cannot be null.");
this.data = data;
bones = new Array<Bone>();
for (boneData in data.bones) {
var bone:Bone;
if (boneData.parent == null) {
bone = new Bone(boneData, this, null);
bone = new Bone(boneData, null);
} else {
var parent:Bone = bones[boneData.parent.index];
bone = new Bone(boneData, this, parent);
bone = new Bone(boneData, parent);
parent.children.push(bone);
}
bones.push(bone);
@ -114,31 +129,20 @@ class Skeleton {
slots = new Array<Slot>();
drawOrder = new Array<Slot>();
for (slotData in data.slots) {
var bone = bones[slotData.boneData.index];
var slot:Slot = new Slot(slotData, bone);
var slot = new Slot(slotData, this);
slots.push(slot);
drawOrder.push(slot);
}
ikConstraints = new Array<IkConstraint>();
for (ikConstraintData in data.ikConstraints) {
ikConstraints.push(new IkConstraint(ikConstraintData, this));
physics = new Array<PhysicsConstraint>();
constraints = new Array<Constraint<Dynamic, Dynamic, Dynamic>>();
for (constraintData in data.constraints) {
var constraint = constraintData.create(this);
if (Std.isOfType(constraint, PhysicsConstraint)) physics.push(cast(constraint, PhysicsConstraint));
constraints.push(constraint);
}
transformConstraints = new Array<TransformConstraint>();
for (transformConstraintData in data.transformConstraints) {
transformConstraints.push(new TransformConstraint(transformConstraintData, this));
}
pathConstraints = new Array<PathConstraint>();
for (pathConstraintData in data.pathConstraints) {
pathConstraints.push(new PathConstraint(pathConstraintData, this));
}
physicsConstraints = new Array<PhysicsConstraint>();
for (physicConstraintData in data.physicsConstraints) {
physicsConstraints.push(new PhysicsConstraint(physicConstraintData, this));
}
color = new Color(1, 1, 1, 1);
updateCache();
}
@ -147,14 +151,19 @@ class Skeleton {
* constraints, or weighted path attachments are added or removed. */
public function updateCache():Void {
_updateCache.resize(0);
resetCache.resize(0);
for (slot in slots)
slot.applied = slot.pose;
for (bone in bones) {
bone.sorted = bone.data.skinRequired;
bone.active = !bone.sorted;
bone.applied = cast(bone.pose, BonePose);
}
if (skin != null) {
var skinBones:Array<BoneData> = skin.bones;
var skinBones = skin.bones;
for (i in 0...skin.bones.length) {
var bone:Bone = bones[skinBones[i].index];
do {
@ -165,216 +174,54 @@ class Skeleton {
}
}
// IK first, lowest hierarchy depth first.
var ikCount:Int = ikConstraints.length;
var transformCount:Int = transformConstraints.length;
var pathCount:Int = pathConstraints.length;
var physicCount:Int = physicsConstraints.length;
var constraintCount:Int = ikCount + transformCount + pathCount + physicCount;
var continueOuter:Bool;
for (i in 0...constraintCount) {
continueOuter = false;
for (ikConstraint in ikConstraints) {
if (ikConstraint.data.order == i) {
sortIkConstraint(ikConstraint);
continueOuter = true;
break;
}
}
if (continueOuter)
continue;
for (transformConstraint in transformConstraints) {
if (transformConstraint.data.order == i) {
sortTransformConstraint(transformConstraint);
continueOuter = true;
break;
}
}
if (continueOuter)
continue;
for (pathConstraint in pathConstraints) {
if (pathConstraint.data.order == i) {
sortPathConstraint(pathConstraint);
break;
}
}
if (continueOuter)
continue;
for (physicConstraint in physicsConstraints) {
if (physicConstraint.data.order == i) {
sortPhysicsConstraint(physicConstraint);
break;
}
}
for (constraint in constraints)
constraint.applied = constraint.pose;
for (c in constraints) {
var constraint:Constraint<Dynamic, Dynamic, Dynamic> = c;
constraint.active = constraint.isSourceActive()
&& (!constraint.data.skinRequired || (skin != null && contains(skin.constraints, constraint.data)));
if (constraint.active) constraint.sort(this);
}
for (bone in bones) {
for (bone in bones)
sortBone(bone);
var updateCache = this._updateCache;
var n = updateCache.length;
for (i in 0...n) {
var updatable = updateCache[i];
if (Std.isOfType(updatable, Bone)) {
var b:Bone = cast updatable;
updateCache[i] = b.applied;
}
}
}
private static function contains(list:Array<ConstraintData>, element:ConstraintData):Bool {
private static function contains(list:Array<ConstraintData<Dynamic, Dynamic>>, element:ConstraintData<Dynamic, Dynamic>):Bool {
return list.indexOf(element) != -1;
}
private function sortIkConstraint(constraint:IkConstraint):Void {
constraint.active = constraint.target.isActive()
&& (!constraint.data.skinRequired || (this.skin != null && contains(this.skin.constraints, constraint.data)));
if (!constraint.active)
return;
var target:Bone = constraint.target;
sortBone(target);
var constrained:Array<Bone> = constraint.bones;
var parent:Bone = constrained[0];
sortBone(parent);
if (constrained.length == 1) {
_updateCache.push(constraint);
sortReset(parent.children);
} else {
var child:Bone = constrained[constrained.length - 1];
sortBone(child);
_updateCache.push(constraint);
sortReset(parent.children);
child.sorted = true;
}
_updateCache.push(constraint);
sortReset(parent.children);
constrained[constrained.length - 1].sorted = true;
}
private function sortPathConstraint(constraint:PathConstraint):Void {
constraint.active = constraint.target.bone.isActive()
&& (!constraint.data.skinRequired || (this.skin != null && contains(this.skin.constraints, constraint.data)));
if (!constraint.active)
return;
var slot:Slot = constraint.target;
var slotIndex:Int = slot.data.index;
var slotBone:Bone = slot.bone;
if (skin != null)
sortPathConstraintAttachment(skin, slotIndex, slotBone);
if (data.defaultSkin != null && data.defaultSkin != skin) {
sortPathConstraintAttachment(data.defaultSkin, slotIndex, slotBone);
}
for (i in 0...data.skins.length) {
sortPathConstraintAttachment(data.skins[i], slotIndex, slotBone);
}
var attachment:Attachment = slot.attachment;
if (Std.isOfType(attachment, PathAttachment))
sortPathConstraintAttachment2(attachment, slotBone);
var constrainedBones:Array<Bone> = constraint.bones;
for (bone in constrainedBones) {
sortBone(bone);
}
_updateCache.push(constraint);
for (bone in constrainedBones) {
sortReset(bone.children);
}
for (bone in constrainedBones) {
bone.sorted = true;
public function constrained (object:Posed<Dynamic, Dynamic, Dynamic>) {
if (object.pose == object.applied) {
object.applied = object.constrained;
resetCache.push(object);
}
}
private function sortTransformConstraint(constraint:TransformConstraint):Void {
constraint.active = constraint.target.isActive()
&& (!constraint.data.skinRequired || (this.skin != null && contains(this.skin.constraints, constraint.data)));
if (!constraint.active)
return;
sortBone(constraint.target);
var constrainedBones:Array<Bone> = constraint.bones;
if (constraint.data.local) {
for (bone in constrainedBones) {
sortBone(bone.parent);
sortBone(bone);
}
} else {
for (bone in constrainedBones) {
sortBone(bone);
}
}
_updateCache.push(constraint);
for (bone in constrainedBones) {
sortReset(bone.children);
}
for (bone in constrainedBones) {
bone.sorted = true;
}
}
private function sortPathConstraintAttachment(skin:Skin, slotIndex:Int, slotBone:Bone):Void {
var dict:StringMap<Attachment> = skin.attachments[slotIndex];
if (dict != null) {
for (attachment in dict.keyValueIterator()) {
sortPathConstraintAttachment2(attachment.value, slotBone);
}
}
}
private function sortPathConstraintAttachment2(attachment:Attachment, slotBone:Bone):Void {
var pathAttachment:PathAttachment = cast(attachment, PathAttachment);
if (pathAttachment == null)
return;
var pathBones:Array<Int> = pathAttachment.bones;
if (pathBones == null) {
sortBone(slotBone);
} else {
var i:Int = 0;
var n:Int = pathBones.length;
while (i < n) {
var nn:Int = pathBones[i++];
nn += i;
while (i < nn) {
sortBone(bones[pathBones[i++]]);
}
}
}
}
private function sortPhysicsConstraint (constraint: PhysicsConstraint) {
var bone:Bone = constraint.bone;
constraint.active = bone.active && (!constraint.data.skinRequired || (skin != null && contains(skin.constraints, constraint.data)));
if (!constraint.active) return;
sortBone(bone);
_updateCache.push(constraint);
sortReset(bone.children);
bone.sorted = true;
}
private function sortBone(bone:Bone):Void {
if (bone.sorted)
return;
var parent:Bone = bone.parent;
if (parent != null)
sortBone(parent);
public function sortBone(bone:Bone):Void {
if (bone.sorted || !bone.active) return;
var parent = bone.parent;
if (parent != null) sortBone(parent);
bone.sorted = true;
_updateCache.push(bone);
}
private function sortReset(bones:Array<Bone>):Void {
public function sortReset(bones:Array<Bone>):Void {
for (bone in bones) {
if (!bone.active)
continue;
if (bone.sorted)
sortReset(bone.children);
bone.sorted = false;
if (bone.active) {
if (bone.sorted) sortReset(bone.children);
bone.sorted = false;
}
}
}
@ -383,124 +230,58 @@ class Skeleton {
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
*/
public function updateWorldTransform(physics:Physics):Void {
if (physics == null) throw new SpineException("physics is undefined");
for (bone in bones) {
bone.ax = bone.x;
bone.ay = bone.y;
bone.arotation = bone.rotation;
bone.ascaleX = bone.scaleX;
bone.ascaleY = bone.scaleY;
bone.ashearX = bone.shearX;
bone.ashearY = bone.shearY;
}
_update++;
for (updatable in _updateCache) {
updatable.update(physics);
}
}
for (object in resetCache)
object.applied.set(object.pose);
/** Temporarily sets the root bone as a child of the specified bone, then updates the world transform for each bone and applies
* all constraints.
*
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
*/
public function updateWorldTransformWith(physics:Physics, parent:Bone):Void {
// Apply the parent bone transform to the root bone. The root bone always inherits scale, rotation and reflection.
var rootBone:Bone = rootBone;
var pa:Float = parent.a,
pb:Float = parent.b,
pc:Float = parent.c,
pd:Float = parent.d;
rootBone.worldX = pa * x + pb * y + parent.worldX;
rootBone.worldY = pc * x + pd * y + parent.worldY;
var rx:Float = (rootBone.rotation + rootBone.shearX) * MathUtils.degRad;
var ry:Float = (rootBone.rotation + 90 + rootBone.shearY) * MathUtils.degRad;
var la:Float = Math.cos(rx) * rootBone.scaleX;
var lb:Float = Math.cos(ry) * rootBone.scaleY;
var lc:Float = Math.sin(rx) * rootBone.scaleX;
var ld:Float = Math.sin(ry) * rootBone.scaleY;
rootBone.a = (pa * la + pb * lc) * scaleX;
rootBone.b = (pa * lb + pb * ld) * scaleX;
rootBone.c = (pc * la + pd * lc) * scaleY;
rootBone.d = (pc * lb + pd * ld) * scaleY;
// Update everything except root bone.
for (updatable in _updateCache) {
if (updatable != rootBone)
updatable.update(physics);
}
for (updatable in _updateCache)
updatable.update(this, physics);
}
/** Sets the bones, constraints, slots, and draw order to their setup pose values. */
public function setToSetupPose():Void {
setBonesToSetupPose();
setSlotsToSetupPose();
public function setupPose():Void {
setupPoseBones();
setupPoseSlots();
}
/** Sets the bones and constraints to their setup pose values. */
public function setBonesToSetupPose():Void {
for (bone in this.bones) bone.setToSetupPose();
for (constraint in this.ikConstraints) constraint.setToSetupPose();
for (constraint in this.transformConstraints) constraint.setToSetupPose();
for (constraint in this.pathConstraints) constraint.setToSetupPose();
for (constraint in this.physicsConstraints) constraint.setToSetupPose();
public function setupPoseBones():Void {
for (bone in this.bones) bone.setupPose();
for (constraint in this.constraints) constraint.setupPose();
}
/** Sets the slots and draw order to their setup pose values. */
public function setSlotsToSetupPose():Void {
public function setupPoseSlots():Void {
var i:Int = 0;
for (slot in slots) {
drawOrder[i++] = slot;
slot.setToSetupPose();
slot.setupPose();
}
}
/** The skeleton's setup pose data. */
public var data(get, never):SkeletonData;
private function get_data():SkeletonData {
return _data;
}
/** The list of bones and constraints, sorted in the order they should be updated, as computed by Skeleton.updateCache(). */
public var getUpdateCache(get, never):Array<Updatable>;
private function get_getUpdateCache():Array<Updatable> {
return _updateCache;
}
/** Returns the root bone, or null if the skeleton has no bones. */
public var rootBone(get, never):Bone;
private function get_rootBone():Bone {
if (bones.length == 0)
return null;
return bones[0];
return bones.length == 0 ? null : bones[0];
}
/** Finds a bone by comparing each bone's name. It is more efficient to cache the results of this method than to call it
* repeatedly. */
public function findBone(boneName:String):Bone {
if (boneName == null) {
throw new SpineException("boneName cannot be null.");
}
for (bone in bones) {
if (bone.data.name == boneName)
return bone;
}
if (boneName == null) throw new SpineException("boneName cannot be null.");
for (bone in bones)
if (bone.data.name == boneName) return bone;
return null;
}
/** @return -1 if the bone was not found. */
public function findBoneIndex(boneName:String):Int {
if (boneName == null) {
throw new SpineException("boneName cannot be null.");
}
if (boneName == null) throw new SpineException("boneName cannot be null.");
var i:Int = 0;
for (bone in bones) {
if (bone.data.name == boneName)
return i;
if (bone.data.name == boneName) return i;
i++;
}
return -1;
@ -509,13 +290,9 @@ class Skeleton {
/** Finds a slot by comparing each slot's name. It is more efficient to cache the results of this method than to call it
* repeatedly. */
public function findSlot(slotName:String):Slot {
if (slotName == null) {
throw new SpineException("slotName cannot be null.");
}
for (slot in slots) {
if (slot.data.name == slotName)
return slot;
}
if (slotName == null) throw new SpineException("slotName cannot be null.");
for (slot in slots)
if (slot.data.name == slotName) return slot;
return null;
}
@ -535,14 +312,7 @@ class Skeleton {
/** @return May be null. */
private function get_skinName():String {
return _skin == null ? null : _skin.name;
}
/** The skeleton's current skin. */
public var skin(get, set):Skin;
private function get_skin():Skin {
return _skin;
return skin == null ? null : skin.name;
}
/** Sets the skin used to look up attachments before looking in the spine.SkeletonData default skin. If the
@ -556,7 +326,7 @@ class Skeleton {
* skeleton is rendered to allow any attachment keys in the current animation(s) to hide or show attachments from the new
* skin. */
private function set_skin(newSkin:Skin):Skin {
if (newSkin == _skin)
if (newSkin == skin)
return null;
if (newSkin != null) {
if (skin != null) {
@ -567,16 +337,15 @@ class Skeleton {
var name:String = slot.data.attachmentName;
if (name != null) {
var attachment:Attachment = newSkin.getAttachment(i, name);
if (attachment != null)
slot.attachment = attachment;
if (attachment != null) slot.pose.attachment = attachment;
}
i++;
}
}
}
_skin = newSkin;
skin = newSkin;
updateCache();
return _skin;
return skin;
}
/** Finds an attachment by looking in the Skeleton.skin and spine.SkeletonData defaultSkin using the slot name and attachment
@ -621,7 +390,7 @@ class Skeleton {
throw new SpineException("Attachment not found: " + attachmentName + ", for slot: " + slotName);
}
}
slot.attachment = attachment;
slot.pose.attachment = attachment;
return;
}
i++;
@ -629,110 +398,66 @@ class Skeleton {
throw new SpineException("Slot not found: " + slotName);
}
/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
* than to call it repeatedly. */
public function findIkConstraint(constraintName:String):IkConstraint {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (ikConstraint in ikConstraints) {
if (ikConstraint.data.name == constraintName)
return ikConstraint;
}
public function findConstraint<T:Constraint<Dynamic, Dynamic, Dynamic>>(constraintName:String, type:Class<T>):Null<T> {
if (constraintName == null) throw new SpineException("constraintName cannot be null.");
if (type == null) throw new SpineException("type cannot be null.");
for (constraint in constraints)
if (Std.isOfType(constraint, type) && constraint.data.name == constraintName) return Std.downcast(constraint, type);
return null;
}
/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
* this method than to call it repeatedly. */
public function findTransformConstraint(constraintName:String):TransformConstraint {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (transformConstraint in transformConstraints) {
if (transformConstraint.data.name == constraintName)
return transformConstraint;
}
return null;
}
/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
* than to call it repeatedly. */
public function findPathConstraint(constraintName:String):PathConstraint {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (pathConstraint in pathConstraints) {
if (pathConstraint.data.name == constraintName)
return pathConstraint;
}
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 repeatedly. */
public function findPhysicsConstraint(constraintName:String):PhysicsConstraint {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (physicsConstraint in physicsConstraints) {
if (physicsConstraint.data.name == constraintName)
return physicsConstraint;
}
return null;
}
public function toString():String {
return _data.name != null ? _data.name : "Skeleton?";
}
private var _tempVertices = new Array<Float>();
private var _bounds = new Rectangle();
/** Returns the axis aligned bounding box (AABB) of the region and mesh attachments for the current pose. Optionally applies
* clipping. */
public function getBounds(clipper: SkeletonClipping = null):Rectangle {
var minX:Float = Math.POSITIVE_INFINITY;
var minY:Float = Math.POSITIVE_INFINITY;
var maxX:Float = Math.NEGATIVE_INFINITY;
var maxY:Float = Math.NEGATIVE_INFINITY;
var minX = Math.POSITIVE_INFINITY;
var minY = Math.POSITIVE_INFINITY;
var maxX = Math.NEGATIVE_INFINITY;
var maxY = Math.NEGATIVE_INFINITY;
for (slot in drawOrder) {
var verticesLength:Int = 0;
var vertices:Array<Float> = null;
var triangles:Array<Int> = null;
var attachment:Attachment = slot.attachment;
if (Std.isOfType(attachment, RegionAttachment)) {
verticesLength = 8;
_tempVertices.resize(verticesLength);
vertices = _tempVertices;
cast(attachment, RegionAttachment).computeWorldVertices(slot, vertices, 0, 2);
triangles = Skeleton.quadTriangles;
} else if (Std.isOfType(attachment, MeshAttachment)) {
var mesh:MeshAttachment = cast(attachment, MeshAttachment);
verticesLength = mesh.worldVerticesLength;
_tempVertices.resize(verticesLength);
vertices = _tempVertices;
mesh.computeWorldVertices(slot, 0, verticesLength, vertices, 0, 2);
triangles = mesh.triangles;
} else if (Std.isOfType(attachment, ClippingAttachment) && clipper != null) {
clipper.clipStart(slot, cast(attachment, ClippingAttachment));
continue;
}
if (vertices != null) {
if (clipper != null && clipper.isClipping()) {
clipper.clipTriangles(vertices, triangles, triangles.length);
vertices = clipper.clippedVertices;
verticesLength = clipper.clippedVertices.length;
var attachment:Attachment = slot.pose.attachment;
if (attachment != null) {
if (Std.isOfType(attachment, RegionAttachment)) {
verticesLength = 8;
_tempVertices.resize(verticesLength);
vertices = _tempVertices;
cast(attachment, RegionAttachment).computeWorldVertices(slot, vertices, 0, 2);
triangles = Skeleton.quadTriangles;
} else if (Std.isOfType(attachment, MeshAttachment)) {
var mesh:MeshAttachment = cast(attachment, MeshAttachment);
verticesLength = mesh.worldVerticesLength;
_tempVertices.resize(verticesLength);
vertices = _tempVertices;
mesh.computeWorldVertices(this, slot, 0, verticesLength, vertices, 0, 2);
triangles = mesh.triangles;
} else if (Std.isOfType(attachment, ClippingAttachment) && clipper != null) {
clipper.clipEnd(slot);
clipper.clipStart(this, slot, cast(attachment, ClippingAttachment));
continue;
}
var ii:Int = 0;
var nn:Int = vertices.length;
while (ii < nn) {
var x:Float = vertices[ii], y:Float = vertices[ii + 1];
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
ii += 2;
if (vertices != null) {
if (clipper != null && clipper.isClipping() && clipper.clipTriangles(vertices, triangles, triangles.length)) {
vertices = clipper.clippedVertices;
verticesLength = clipper.clippedVertices.length;
}
var ii:Int = 0;
var nn:Int = vertices.length;
while (ii < nn) {
var x:Float = vertices[ii], y:Float = vertices[ii + 1];
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
ii += 2;
}
}
if (clipper != null) clipper.clipEnd(slot);
}
if (clipper != null) clipper.clipEndWithSlot(slot);
}
if (clipper != null) clipper.clipEnd();
_bounds.x = minX;
@ -749,13 +474,17 @@ class Skeleton {
/** Calls spine.PhysicsConstraint.translate() for each physics constraint. */
public function physicsTranslate (x:Float, y:Float):Void {
for (physicsConstraint in physicsConstraints)
for (physicsConstraint in physics)
physicsConstraint.translate(x, y);
}
/** Calls spine.PhysicsConstraint.rotate() for each physics constraint. */
public function physicsRotate (x:Float, y:Float, degrees:Float):Void {
for (physicsConstraint in physicsConstraints)
for (physicsConstraint in physics)
physicsConstraint.rotate(x, y, degrees);
}
public function toString():String {
return data.name != null ? data.name : "Skeleton?";
}
}

View File

@ -32,27 +32,29 @@ package spine;
import spine.attachments.ClippingAttachment;
class SkeletonClipping {
private var triangulator:Triangulator = new Triangulator();
private var clippingPolygon:Array<Float> = new Array<Float>();
private var clipOutput:Array<Float> = new Array<Float>();
private var triangulator = new Triangulator();
private var clippingPolygon = new Array<Float>();
private var clipOutput = new Array<Float>();
public var clippedVertices:Array<Float> = new Array<Float>();
public var clippedUvs:Array<Float> = new Array<Float>();
public var clippedTriangles:Array<Int> = new Array<Int>();
public var clippedVertices = new Array<Float>();
public var clippedUvs = new Array<Float>();
public var clippedTriangles = new Array<Int>();
private var scratch:Array<Float> = new Array<Float>();
private var scratch = new Array<Float>();
private var clipAttachment:ClippingAttachment;
private var clippingPolygons:Array<Array<Float>>;
public function new() {}
public function clipStart(slot:Slot, clip:ClippingAttachment):Int {
if (clipAttachment != null)
return 0;
public function clipStart(skeleton:Skeleton, slot:Slot, clip:ClippingAttachment):Int {
if (clipAttachment != null) return 0;
var n = clip.worldVerticesLength;
if (n < 6) return 0;
clipAttachment = clip;
clippingPolygon.resize(clip.worldVerticesLength);
clip.computeWorldVertices(slot, 0, clippingPolygon.length, clippingPolygon, 0, 2);
clippingPolygon.resize(n);
clip.computeWorldVertices(skeleton, slot, 0, n, clippingPolygon, 0, 2);
SkeletonClipping.makeClockwise(clippingPolygon);
clippingPolygons = triangulator.decompose(clippingPolygon, triangulator.triangulate(clippingPolygon));
for (polygon in clippingPolygons) {
@ -63,14 +65,8 @@ class SkeletonClipping {
return clippingPolygons.length;
}
public function clipEndWithSlot(slot:Slot):Void {
if (clipAttachment != null && clipAttachment.endSlot == slot.data)
clipEnd();
}
public function clipEnd():Void {
if (clipAttachment == null)
return;
public function clipEnd(?slot:Slot):Void {
if (clipAttachment == null || (slot != null && clipAttachment.endSlot != slot.data)) return;
clipAttachment = null;
clippingPolygons = null;
clippedVertices.resize(0);
@ -84,36 +80,34 @@ class SkeletonClipping {
return clipAttachment != null;
}
private function clipTrianglesNoRender(vertices:Array<Float>, triangles:Array<Int>, trianglesLength:Float):Void {
private function clipTrianglesNoRender(vertices:Array<Float>, triangles:Array<Int>, trianglesLength:Float):Bool {
var polygonsCount:Int = clippingPolygons.length;
var index:Int = 0;
clippedVertices.resize(0);
clippedTriangles.resize(0);
var i:Int = 0;
var clipOutputItems:Array<Float> = null;
while (i < trianglesLength) {
var vertexOffset:Int = triangles[i] << 1;
var x1:Float = vertices[vertexOffset],
y1:Float = vertices[vertexOffset + 1];
var v:Int = triangles[i] << 1;
var x1:Float = vertices[v], y1:Float = vertices[v + 1];
vertexOffset = triangles[i + 1] << 1;
var x2:Float = vertices[vertexOffset],
y2:Float = vertices[vertexOffset + 1];
v = triangles[i + 1] << 1;
var x2:Float = vertices[v], y2:Float = vertices[v + 1];
vertexOffset = triangles[i + 2] << 1;
var x3:Float = vertices[vertexOffset],
y3:Float = vertices[vertexOffset + 1];
v = triangles[i + 2] << 1;
var x3:Float = vertices[v], y3:Float = vertices[v + 1];
for (p in 0...polygonsCount) {
var s:Int = clippedVertices.length;
var clippedVerticesItems:Array<Float>;
var clippedTrianglesItems:Array<Int>;
if (this.clip(x1, y1, x2, y2, x3, y3, clippingPolygons[p], clipOutput)) {
clipOutputItems = clipOutput;
var clipOutputLength:Int = clipOutput.length;
if (clipOutputLength == 0)
continue;
var clipOutputCount:Int = clipOutputLength >> 1;
var clipOutputItems:Array<Float> = clipOutput;
clippedVerticesItems = clippedVertices;
clippedVerticesItems.resize(s + clipOutputLength);
var ii:Int = 0;
@ -160,12 +154,12 @@ class SkeletonClipping {
i += 3;
}
return clipOutputItems != null;
}
public function clipTriangles(vertices:Array<Float>, triangles:Array<Int>, trianglesLength:Float, uvs:Array<Float> = null):Void {
public function clipTriangles(vertices:Array<Float>, triangles:Array<Int>, trianglesLength:Float, uvs:Array<Float> = null, stride:Int = 0):Bool {
if (uvs == null) {
clipTrianglesNoRender(vertices, triangles, trianglesLength);
return;
return clipTrianglesNoRender(vertices, triangles, trianglesLength);
}
var polygonsCount:Int = clippingPolygons.length;
@ -174,21 +168,19 @@ class SkeletonClipping {
clippedUvs.resize(0);
clippedTriangles.resize(0);
var i:Int = 0;
var clipOutputItems:Array<Float> = null;
while (i < trianglesLength) {
var vertexOffset:Int = triangles[i] << 1;
var x1:Float = vertices[vertexOffset],
y1:Float = vertices[vertexOffset + 1];
var u1:Float = uvs[vertexOffset], v1:Float = uvs[vertexOffset + 1];
var t:Int = triangles[i];
var u1:Float = uvs[t << 1], v1:Float = uvs[(t << 1) + 1];
var x1:Float = vertices[t * stride], y1:Float = vertices[t * stride + 1];
vertexOffset = triangles[i + 1] << 1;
var x2:Float = vertices[vertexOffset],
y2:Float = vertices[vertexOffset + 1];
var u2:Float = uvs[vertexOffset], v2:Float = uvs[vertexOffset + 1];
t = triangles[i + 1];
var u2:Float = uvs[t << 1], v2:Float = uvs[(t << 1) + 1];
var x2:Float = vertices[t * stride], y2:Float = vertices[t * stride + 1];
vertexOffset = triangles[i + 2] << 1;
var x3:Float = vertices[vertexOffset],
y3:Float = vertices[vertexOffset + 1];
var u3:Float = uvs[vertexOffset], v3:Float = uvs[vertexOffset + 1];
t = triangles[i + 2];
var u3:Float = uvs[t << 1], v3:Float = uvs[(t << 1) + 1];
var x3:Float = vertices[t * stride], y3:Float = vertices[t * stride + 1];
for (p in 0...polygonsCount) {
var s:Int = clippedVertices.length;
@ -196,6 +188,7 @@ class SkeletonClipping {
var clippedUvsItems:Array<Float>;
var clippedTrianglesItems:Array<Int>;
if (this.clip(x1, y1, x2, y2, x3, y3, clippingPolygons[p], clipOutput)) {
clipOutputItems = clipOutput;
var clipOutputLength:Int = clipOutput.length;
if (clipOutputLength == 0)
continue;
@ -206,9 +199,8 @@ class SkeletonClipping {
var d:Float = 1 / (d0 * d2 + d1 * (y1 - y3));
var clipOutputCount:Int = clipOutputLength >> 1;
var clipOutputItems:Array<Float> = clipOutput;
clippedVerticesItems = clippedVertices;
clippedVerticesItems.resize(s + clipOutputLength);
clippedVerticesItems.resize(s + clipOutputLength * stride);
clippedUvsItems = clippedUvs;
clippedUvsItems.resize(s + clipOutputLength);
@ -242,7 +234,7 @@ class SkeletonClipping {
index += clipOutputCount + 1;
} else {
clippedVerticesItems = clippedVertices;
clippedVerticesItems.resize(s + 3 * 2);
clippedVerticesItems.resize(s + 3 * stride);
clippedVerticesItems[s] = x1;
clippedVerticesItems[s + 1] = y1;
clippedVerticesItems[s + 2] = x2;
@ -272,13 +264,14 @@ class SkeletonClipping {
i += 3;
}
return clipOutputItems != null;
}
/**
* Clips the input triangle against the convex, clockwise clipping area. If the triangle lies entirely within the clipping
* area, false is returned. The clipping area must duplicate the first vertex at the end of the vertices list.
*/
public function clip(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float, clippingArea:Array<Float>, output:Array<Float>):Bool {
private function clip(x1:Float, y1:Float, x2:Float, y2:Float, x3:Float, y3:Float, clippingArea:Array<Float>, output:Array<Float>):Bool {
var originalOutput:Array<Float> = output;
var clipped:Bool = false;

View File

@ -37,55 +37,66 @@ import spine.attachments.AtlasAttachmentLoader;
/** Stores the setup pose and all of the stateless data for a skeleton.
*
*
*
* @see https://esotericsoftware.com/spine-runtime-architecture#Data-objects Data objects in the Spine Runtimes
* Guide. */
class SkeletonData {
/** The skeleton's name, which by default is the name of the skeleton data file when possible, or null when a name hasn't been
* set. */
public var name:String;
public var name:String = null;
/** The skeleton's bones, sorted parent first. The root bone is always the first bone. */
public var bones:Array<BoneData> = new Array<BoneData>(); // Ordered parents first.
public final bones = new Array<BoneData>(); // Ordered parents first.
/** The skeleton's slots in the setup pose draw order. */
public var slots:Array<SlotData> = new Array<SlotData>(); // Setup pose draw order.
public final slots = new Array<SlotData>(); // Setup pose draw order.
/** All skins, including the default skin. */
public var skins:Array<Skin> = new Array<Skin>();
public final skins = new Array<Skin>();
/** The skeleton's default skin. By default this skin contains all attachments that were not in a skin in Spine.
*
* See Skeleton#getAttachment(int, String). */
public var defaultSkin:Skin;
public var defaultSkin:Skin = null;
/** The skeleton's events. */
public var events:Array<EventData> = new Array<EventData>();
public var events = new Array<EventData>();
/** The skeleton's animations. */
public var animations:Array<Animation> = new Array<Animation>();
/** The skeleton's IK constraints. */
public var ikConstraints:Array<IkConstraintData> = new Array<IkConstraintData>();
/** The skeleton's transform constraints. */
public var transformConstraints:Array<TransformConstraintData> = new Array<TransformConstraintData>();
/** The skeleton's path constraints. */
public var pathConstraints:Array<PathConstraintData> = new Array<PathConstraintData>();
/** The skeleton's physics constraints. */
public var physicsConstraints:Array<PhysicsConstraintData> = new Array<PhysicsConstraintData>();
public var animations = new Array<Animation>();
/** The skeleton's constraints. */
public var constraints = new Array<ConstraintData<Dynamic, Dynamic>>();
/** The X coordinate of the skeleton's axis aligned bounding box in the setup pose. */
public var x:Float = 0;
/** The Y coordinate of the skeleton's axis aligned bounding box in the setup pose. */
public var y:Float = 0;
/** The width of the skeleton's axis aligned bounding box in the setup pose. */
public var width:Float = 0;
/** The height of the skeleton's axis aligned bounding box in the setup pose. */
public var height:Float = 0;
/** Baseline scale factor for applying physics and other effects based on distance to non-scalable properties, such as angle or
* scale. Default is 100. */
public var referenceScale:Float = 100;
/** The Spine version used to export the skeleton data, or null. */
public var version:String;
/** The skeleton data hash. This value will change if any of the skeleton data has changed. */
public var hash:String;
// Nonessential.
/** The dopesheet FPS in Spine, or zero if nonessential data was not exported. */
public var fps:Float = 0;
/** The path to the images directory as defined in Spine, or null if nonessential data was not exported. */
public var imagesPath:String;
/** The path to the audio directory as defined in Spine, or null if nonessential data was not exported. */
public var audioPath:String;
@ -112,13 +123,9 @@ class SkeletonData {
* @param boneName The name of the bone to find.
* @return May be null. */
public function findBone(boneName:String):BoneData {
if (boneName == null)
throw new SpineException("boneName cannot be null.");
for (i in 0...bones.length) {
var bone:BoneData = bones[i];
if (bone.name == boneName)
return bone;
}
if (boneName == null) throw new SpineException("boneName cannot be null.");
for (bone in bones)
if (bone.name == boneName) return bone;
return null;
}
@ -142,13 +149,9 @@ class SkeletonData {
* @param slotName The name of the slot to find.
* @return May be null. */
public function findSlot(slotName:String):SlotData {
if (slotName == null)
throw new SpineException("slotName cannot be null.");
for (i in 0...slots.length) {
var slot:SlotData = slots[i];
if (slot.name == slotName)
return slot;
}
if (slotName == null) throw new SpineException("slotName cannot be null.");
for (slot in slots)
if (slot.name == slotName) return slot;
return null;
}
@ -159,12 +162,9 @@ class SkeletonData {
* @param skinName The name of the skin to find.
* @return May be null. */
public function findSkin(skinName:String):Skin {
if (skinName == null)
throw new SpineException("skinName cannot be null.");
for (skin in skins) {
if (skin.name == skinName)
return skin;
}
if (skinName == null) throw new SpineException("skinName cannot be null.");
for (skin in skins)
if (skin.name == skinName) return skin;
return null;
}
@ -175,12 +175,9 @@ class SkeletonData {
* @param eventName The name of the event to find.
* @return May be null. */
public function findEvent(eventName:String):EventData {
if (eventName == null)
throw new SpineException("eventName cannot be null.");
for (eventData in events) {
if (eventData.name == eventName)
return eventData;
}
if (eventName == null) throw new SpineException("eventName cannot be null.");
for (eventData in events)
if (eventData.name == eventName) return eventData;
return null;
}
@ -191,120 +188,25 @@ class SkeletonData {
* @param animationName The name of the animation to find.
* @return May be null. */
public function findAnimation(animationName:String):Animation {
if (animationName == null)
throw new SpineException("animationName cannot be null.");
for (animation in animations) {
if (animation.name == animationName)
return animation;
}
if (animationName == null) throw new SpineException("animationName cannot be null.");
for (animation in animations)
if (animation.name == animationName) return animation;
return null;
}
// --- IK constraints.
// --- Constraints.
/** Finds an IK constraint by comparing each IK constraint's name. It is more efficient to cache the results of this method
* than to call it multiple times.
* @param constraintName The name of the IK constraint to find.
* @return May be null. */
public function findIkConstraint(constraintName:String):IkConstraintData {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (ikConstraintData in ikConstraints) {
if (ikConstraintData.name == constraintName)
return ikConstraintData;
public function findConstraint<T:ConstraintData<Dynamic, Dynamic>>(constraintName:String, type:Class<T>):Null<T> {
if (constraintName == null) throw new SpineException("constraintName cannot be null.");
if (type == null) throw new SpineException("type cannot be null.");
for (constraint in constraints) {
if (Std.is(constraint, type) && constraint.name == constraintName)
return Std.downcast(constraint, type);
}
return null;
}
// --- Transform constraints.
/** Finds a transform constraint by comparing each transform constraint's name. It is more efficient to cache the results of
* this method than to call it multiple times.
* @param constraintName The name of the transform constraint to find.
* @return May be null. */
public function findTransformConstraint(constraintName:String):TransformConstraintData {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (transformConstraintData in transformConstraints) {
if (transformConstraintData.name == constraintName)
return transformConstraintData;
}
return null;
}
/** Finds the index of a transform constraint by comparing each transform constraint's name.
* @param transformConstraintName The name of the transform constraint to find.
* @return -1 if the transform constraint was not found. */
public function findTransformConstraintIndex(transformConstraintName:String):Int {
if (transformConstraintName == null)
throw new SpineException("transformConstraintName cannot be null.");
for (i in 0...transformConstraints.length) {
if (transformConstraints[i].name == transformConstraintName)
return i;
}
return -1;
}
// --- Path constraints.
/** Finds a path constraint by comparing each path constraint's name. It is more efficient to cache the results of this method
* than to call it multiple times.
* @param constraintName The name of the path constraint to find.
* @return May be null. */
public function findPathConstraint(constraintName:String):PathConstraintData {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (i in 0...pathConstraints.length) {
var constraint:PathConstraintData = pathConstraints[i];
if (constraint.name == constraintName)
return constraint;
}
return null;
}
/** Finds the index of a path constraint by comparing each path constraint's name.
* @param pathConstraintName The name of the path constraint to find.
* @return -1 if the path constraint was not found. */
public function findPathConstraintIndex(pathConstraintName:String):Int {
if (pathConstraintName == null)
throw new SpineException("pathConstraintName cannot be null.");
for (i in 0...pathConstraints.length) {
if (pathConstraints[i].name == pathConstraintName)
return i;
}
return -1;
}
// --- Physics constraints.
/** 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.
* @param constraintName The name of the physics constraint to find.
* @return May be null. */
public function findPhysicsConstraint(constraintName:String):PhysicsConstraintData {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (i in 0...physicsConstraints.length) {
var constraint:PhysicsConstraintData = physicsConstraints[i];
if (constraint.name == constraintName)
return constraint;
}
return null;
}
/** Finds the index of a physics constraint by comparing each physics constraint's name.
* @param constraintName The name of the physics constraint to find.
* @return -1 if the physics constraint was not found. */
public function findPhysicsConstraintIndex(constraintName:String):Int {
if (constraintName == null)
throw new SpineException("constraintName cannot be null.");
for (i in 0...physicsConstraints.length) {
if (physicsConstraints[i].name == constraintName)
return i;
}
return -1;
}
public function toString():String {
return name;
}

View File

@ -34,31 +34,35 @@ import spine.attachments.Attachment;
import spine.attachments.MeshAttachment;
/** Stores attachments by slot index and attachment name.
*
*
* See spine.SkeletonData.defaultSkin, spine.Skeleton.skin, and
* Runtime skins at https://esotericsoftware.com/spine-runtime-skins in the Spine Runtimes Guide. */
class Skin {
private var _name:String;
private var _attachments:Array<StringMap<Attachment>> = new Array<StringMap<Attachment>>();
private var _bones:Array<BoneData> = new Array<BoneData>();
private var _constraints:Array<ConstraintData> = new Array<ConstraintData>();
private var _color:Color = new Color(0.99607843, 0.61960787, 0.30980393, 1); // fe9e4fff
/** The skin's name, which is unique across all skins in the skeleton. */
public final name:String;
/** Returns the attachment for the specified slot index and name, or null. */
public final attachments:Array<StringMap<Attachment>> = new Array<StringMap<Attachment>>();
public final bones:Array<BoneData> = new Array<BoneData>();
public final constraints = new Array<ConstraintData<Dynamic, Pose<Any>>>();
/** The color of the skin as it was in Spine, or a default color if nonessential data was not exported. */
public final color:Color = new Color(0.99607843, 0.61960787, 0.30980393, 1); // fe9e4fff
public function new(name:String) {
if (name == null)
throw new SpineException("name cannot be null.");
_name = name;
if (name == null) throw new SpineException("name cannot be null.");
this.name = name;
}
/** Adds an attachment to the skin for the specified slot index and name. */
public function setAttachment(slotIndex:Int, name:String, attachment:Attachment):Void {
if (attachment == null)
throw new SpineException("attachment cannot be null.");
if (slotIndex >= _attachments.length)
_attachments.resize(slotIndex + 1);
if (_attachments[slotIndex] == null)
_attachments[slotIndex] = new StringMap<Attachment>();
_attachments[slotIndex].set(name, attachment);
if (attachment == null) throw new SpineException("attachment cannot be null.");
if (slotIndex >= attachments.length)
attachments.resize(slotIndex + 1);
if (attachments[slotIndex] == null)
attachments[slotIndex] = new StringMap<Attachment>();
attachments[slotIndex].set(name, attachment);
}
/** Adds all attachments, bones, and constraints from the specified skin to this skin. */
@ -68,26 +72,26 @@ class Skin {
var bone:BoneData = skin.bones[i];
contained = false;
for (j in 0...bones.length) {
if (_bones[j] == bone) {
if (bones[j] == bone) {
contained = true;
break;
}
}
if (!contained)
_bones.push(bone);
bones.push(bone);
}
for (i in 0...skin.constraints.length) {
var constraint:ConstraintData = skin.constraints[i];
var constraint = skin.constraints[i];
contained = false;
for (j in 0..._constraints.length) {
if (_constraints[j] == constraint) {
for (j in 0...constraints.length) {
if (constraints[j] == constraint) {
contained = true;
break;
}
}
if (!contained)
_constraints.push(constraint);
constraints.push(constraint);
}
var attachments:Array<SkinEntry> = skin.getAttachments();
@ -106,27 +110,27 @@ class Skin {
for (i in 0...skin.bones.length) {
var bone:BoneData = skin.bones[i];
contained = false;
for (j in 0..._bones.length) {
if (_bones[j] == bone) {
for (j in 0...bones.length) {
if (bones[j] == bone) {
contained = true;
break;
}
}
if (!contained)
_bones.push(bone);
bones.push(bone);
}
for (i in 0...skin.constraints.length) {
var constraint:ConstraintData = skin.constraints[i];
var constraint = skin.constraints[i];
contained = false;
for (j in 0..._constraints.length) {
if (_constraints[j] == constraint) {
for (j in 0...constraints.length) {
if (constraints[j] == constraint) {
contained = true;
break;
}
}
if (!contained)
_constraints.push(constraint);
constraints.push(constraint);
}
var attachments:Array<SkinEntry> = skin.getAttachments();
@ -147,15 +151,15 @@ class Skin {
/** Returns the attachment for the specified slot index and name, or null. */
public function getAttachment(slotIndex:Int, name:String):Attachment {
if (slotIndex >= _attachments.length)
if (slotIndex >= attachments.length)
return null;
var dictionary:StringMap<Attachment> = _attachments[slotIndex];
var dictionary:StringMap<Attachment> = attachments[slotIndex];
return dictionary != null ? dictionary.get(name) : null;
}
/** Removes the attachment in the skin for the specified slot index and name, if any. */
public function removeAttachment(slotIndex:Int, name:String):Void {
var dictionary:StringMap<Attachment> = _attachments[slotIndex];
var dictionary:StringMap<Attachment> = attachments[slotIndex];
if (dictionary != null)
dictionary.remove(name);
}
@ -163,8 +167,8 @@ class Skin {
/** Returns all attachments in this skin. */
public function getAttachments():Array<SkinEntry> {
var entries:Array<SkinEntry> = new Array<SkinEntry>();
for (slotIndex in 0..._attachments.length) {
var attachments:StringMap<Attachment> = _attachments[slotIndex];
for (slotIndex in 0...attachments.length) {
var attachments:StringMap<Attachment> = attachments[slotIndex];
if (attachments != null) {
for (name in attachments.keys()) {
var attachment:Attachment = attachments.get(name);
@ -179,7 +183,7 @@ class Skin {
/** Returns all attachments in this skin for the specified slot index. */
public function getAttachmentsForSlot(slotIndex:Int):Array<SkinEntry> {
var entries:Array<SkinEntry> = new Array<SkinEntry>();
var attachments:StringMap<Attachment> = _attachments[slotIndex];
var attachments:StringMap<Attachment> = attachments[slotIndex];
if (attachments != null) {
for (name in attachments.keys()) {
var attachment:Attachment = attachments.get(name);
@ -192,54 +196,21 @@ class Skin {
/** Clears all attachments, bones, and constraints. */
public function clear():Void {
_attachments.resize(0);
_bones.resize(0);
_constraints.resize(0);
attachments.resize(0);
bones.resize(0);
constraints.resize(0);
}
public var attachments(get, never):Array<StringMap<Attachment>>;
private function get_attachments():Array<StringMap<Attachment>> {
return _attachments;
public function toString():String {
return name;
}
public var bones(get, never):Array<BoneData>;
private function get_bones():Array<BoneData> {
return _bones;
}
public var constraints(get, never):Array<ConstraintData>;
private function get_constraints():Array<ConstraintData> {
return _constraints;
}
/** The skin's name, which is unique across all skins in the skeleton. */
public var name(get, never):String;
private function get_name():String {
return _name;
}
/** The color of the skin as it was in Spine, or a default color if nonessential data was not exported. */
public var color(get, never):Color;
private function get_color():Color {
return _color;
}
/*
public function toString():String
{
return _name;
}
*/
/** Attach each attachment in this skin if the corresponding attachment in the old skin is currently attached. */
public function attachAll(skeleton:Skeleton, oldSkin:Skin):Void {
var slotIndex:Int = 0;
for (slot in skeleton.slots) {
var slotAttachment:Attachment = slot.attachment;
for (element in skeleton.slots) {
var slot = element.pose;
var slotAttachment = slot.attachment;
if (slotAttachment != null && slotIndex < oldSkin.attachments.length) {
var dictionary:StringMap<Attachment> = oldSkin.attachments[slotIndex];
if (null != dictionary) {
@ -247,8 +218,7 @@ class Skin {
var skinAttachment:Attachment = dictionary.get(name);
if (slotAttachment == skinAttachment) {
var attachment:Attachment = getAttachment(slotIndex, name);
if (attachment != null)
slot.attachment = attachment;
if (attachment != null) slot.attachment = attachment;
break;
}
}

View File

@ -0,0 +1,112 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores the setup pose for a {@link PhysicsConstraint}.
* <p>
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
class Slider extends Constraint<Slider, SliderData, SliderPose> {
static private final offsets:Array<Float> = [for (i in 0...6) .0];
public var bone:Bone;
public function new (data:SliderData, skeleton:Skeleton) {
super(data, new SliderPose(), new SliderPose());
if (skeleton == null) throw new IllegalArgumentException("skeleton cannot be null.");
if (data.bone != null) bone = skeleton.bones.items[data.bone.index];
}
public function copy (skeleton:Skeleton) {
var copy = new Slider(data, skeleton);
copy.pose.set(pose);
return copy;
}
public function update (skeleton:Skeleton, physics:Physics) {
var p = applied;
if (p.mix == 0) return;
var animation = data.animation;
if (bone != null) {
if (!bone.active) return;
if (data.local) bone.applied.validateLocalTransform(skeleton);
p.time = (data.property.value(bone.applied, data.local, offsets) - data.property.offset) * data.scale;
if (data.loop)
p.time = animation.duration + (p.time % animation.duration);
else
p.time = Math.max(0, p.time);
}
var bones = skeleton.bones;
var indices = animation.bones;
var i = 0, n = animation.bones.size;
while (i < n)
bones[indices[i++]].applied.modifyLocal(skeleton);
animation.apply(skeleton, p.time, p.time, data.loop, null, p.mix, data.additive ? MixBlend.add : MixBlend.replace,
MixDirection.mixIn, true);
}
function sort (skeleton:Skeleton) {
if (bone != null && !data.local) skeleton.sortBone(bone);
skeleton.updateCache.add(this);
var bones = skeleton.bones;
var indices = data.animation.bones;
var i = 0, n = data.animation.bones.length;
while (i < n) {
var bone = bones[indices[i++]];
bone.sorted = false;
skeleton.sortReset(bone.children);
skeleton.constrained(bone);
}
var timelines = data.animation.timelines;
var slots = skeleton.slots;
var constraints = skeleton.constraints;
var physics = skeleton.physics;
var physicsCount = skeleton.physics.length;
var i = 0, n = data.animation.timelines.length;
while (i < n) {
var t = timelines[i++];
if (std.isOfType(t, SlotTimeline))
skeleton.constrained(slots[cast(t, SlotTimeline).getSlotIndex()]);
else if (std.isOfType(t, PhysicsConstraintTimeline)) {
if (cast(t, PhysicsConstraintTimeline).constraintIndex == -1) {
for (ii in 0...physicsCount)
skeleton.constrained(physics[ii]);
} else
skeleton.constrained(constraints[timeline.constraintIndex]);
} else if (std.isOfType(t, ConstraintTimeline)) //
skeleton.constrained(constraints[cast(t, ConstraintTimeline).getConstraintIndex()]);
}
}
}

View File

@ -0,0 +1,53 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores the setup pose for a PhysicsConstraint.
*
* See <a href="https://esotericsoftware.com/spine-physics-constraints">Physics constraints</a> in the Spine User Guide. */
class SliderData extends ConstraintData<Slider, SliderPose> {
public var animation:Animation;
public var additive = false;
public var loop = false;
public var bone:BoneData = null;
public var property:FromProperty = null;
public var scale = 1.;
public var local = false;
public function new (name:String) {
super(name, new SliderPose());
}
public function create (skeleton:Skeleton) {
return new Slider(this, skeleton);
}
}

View File

@ -0,0 +1,41 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores a pose for a slider. */
class SliderPose implements Pose<SliderPose> {
public var time = 0.;
public var mix = 0.;
public function set (pose:SliderPose) {
time = pose.time;
mix = pose.mix;
}
}

View File

@ -35,101 +35,51 @@ import spine.attachments.VertexAttachment;
/** Stores a slot's current pose. Slots organize attachments for Skeleton.drawOrder purposes and provide a place to store
* state for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared
* across multiple skeletons. */
class Slot {
private var _data:SlotData;
private var _bone:Bone;
class Slot extends Posed<SlotData, SlotPose, SlotPose> {
/** The color used to tint the slot's attachment. If darkColor is set, this is used as the light color for two
* color tinting. */
public var color:Color;
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
* color's alpha is not used. */
public var darkColor:Color;
private var _attachment:Attachment;
/** The index of the texture region to display when the slot's attachment has a spine.attachments.Sequence. -1 represents the
* Sequence.getSetupIndex(). */
public var sequenceIndex = -1;
public var attachmentState:Int = 0;
/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
* weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
* @see spine.attachments.VertexAttachment.computeWorldVertices()
* @see spine.animation.DeformTimeline */
public var deform:Array<Float> = new Array<Float>();
/** Copy constructor. */
public function new(data:SlotData, bone:Bone) {
if (data == null)
throw new SpineException("data cannot be null.");
if (bone == null)
throw new SpineException("bone cannot be null.");
_data = data;
_bone = bone;
this.color = new Color(1, 1, 1, 1);
this.darkColor = data.darkColor == null ? null : new Color(1, 1, 1, 1);
setToSetupPose();
}
/** The slot's setup pose data. */
public var data(get, never):SlotData;
private function get_data():SlotData {
return _data;
}
public var skeleton:Skeleton;
/** The bone this slot belongs to. */
public var bone(get, never):Bone;
public var bone:Bone;
private function get_bone():Bone {
return _bone;
}
public var attachmentState:Int ;
/** The skeleton this slot belongs to. */
public var skeleton(get, never):Skeleton;
private function get_skeleton():Skeleton {
return _bone.skeleton;
}
/** The current attachment for the slot, or null if the slot has no attachment. */
public var attachment(get, set):Attachment;
private function get_attachment():Attachment {
return _attachment;
}
/** Sets the slot's attachment and, if the attachment changed, resets sequenceIndex and clears the deform.
* The deform is not cleared if the old attachment has the same spine.attachments.VertexAttachment.timelineAttachment as the
* specified attachment. */
public function set_attachment(attachmentNew:Attachment):Attachment {
if (attachment == attachmentNew)
return attachmentNew;
if (!Std.isOfType(attachmentNew, VertexAttachment)
|| !Std.isOfType(attachment, VertexAttachment)
|| cast(attachmentNew, VertexAttachment).timelineAttachment != cast(attachment, VertexAttachment).timelineAttachment) {
deform = new Array<Float>();
public function new(data:SlotData, skeleton:Skeleton) {
super(data, new SlotPose(), new SlotPose());
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
this.skeleton = skeleton;
bone = skeleton.bones[data.boneData.index];
if (data.setup.darkColor != null) {
pose.darkColor = new Color(1, 1, 1, 1);
constrained.darkColor = new Color(1, 1, 1, 1);
}
_attachment = attachmentNew;
sequenceIndex = -1;
return attachmentNew;
setupPose();
}
/** Copy method. */
public function copy(slot:Slot, bone:Bone, skeleton:Skeleton):Slot {
var copy = new Slot(slot.data, skeleton);
if (bone == null) throw new SpineException("bone cannot be null.");
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
this.bone = bone;
if (data.setup.darkColor != null) {
pose.darkColor = new Color(1, 1, 1, 1);
constrained.darkColor = new Color(1, 1, 1, 1);
}
copy.pose.set(slot.pose);
return copy;
}
/** Sets this slot to the setup pose. */
public function setToSetupPose():Void {
color.setFromColor(data.color);
if (darkColor != null)
darkColor.setFromColor(data.darkColor);
if (_data.attachmentName == null) {
attachment = null;
override public function setupPose():Void {
pose.color.setFromColor(data.setup.color);
if (pose.darkColor != null) pose.darkColor.setFromColor(data.setup.darkColor);
pose.sequenceIndex = data.setup.sequenceIndex;
if (data.attachmentName == null) {
pose.attachment = null;
} else {
_attachment = null;
attachment = skeleton.getAttachmentForSlotIndex(data.index, data.attachmentName);
pose.attachment = null;
pose.attachment = skeleton.getAttachmentForSlotIndex(data.index, data.attachmentName);
}
}
public function toString():String {
return _data.name != null ? _data.name : "Slot?";
}
}

View File

@ -30,58 +30,29 @@
package spine;
/** Stores the setup pose for a spine.Slot. */
class SlotData {
private var _index:Int;
private var _name:String;
private var _boneData:BoneData;
class SlotData extends PosedData<SlotPose> {
/** The index of the slot in spine.Skeleton.getSlots(). */
public final index:Int;
/** The bone this slot belongs to. */
public final boneData:BoneData;
/** The color used to tint the slot's attachment. If SlotData.darkColor is set, this is used as the light color for two
* color tinting. */
public var color:Color = new Color(1, 1, 1, 1);
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
* color's alpha is not used. */
public var darkColor:Color = null;
/** The name of the attachment that is visible for this slot in the setup pose, or null if no attachment is visible. */
public var attachmentName:String;
public var attachmentName:String = null;
/** The blend mode for drawing the slot's attachment. */
public var blendMode:BlendMode = BlendMode.normal;
// Nonessential.
/** False if the slot was hidden in Spine and nonessential data was exported. Does not affect runtime rendering. */
public var visible:Bool = true;
public function new(index:Int, name:String, boneData:BoneData) {
if (index < 0)
throw new SpineException("index must be >= 0.");
if (name == null)
throw new SpineException("name cannot be null.");
if (boneData == null)
throw new SpineException("boneData cannot be null.");
_index = index;
_name = name;
_boneData = boneData;
}
/** The index of the slot in spine.Skeleton.getSlots(). */
public var index(get, never):Int;
private function get_index():Int {
return _index;
}
/** The name of the slot, which is unique across all slots in the skeleton. */
public var name(get, never):String;
private function get_name():String {
return _name;
}
/** The bone this slot belongs to. */
public var boneData(get, never):BoneData;
private function get_boneData():BoneData {
return _boneData;
}
public function toString():String {
return _name;
super(name, new SlotPose());
if (index < 0) throw new SpineException("index must be >= 0.");
if (boneData == null) throw new SpineException("boneData cannot be null.");
this.index = index;
this.boneData = boneData;
}
}

View File

@ -0,0 +1,87 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
import spine.attachments.Attachment;
import spine.attachments.VertexAttachment;
/** Stores a slot's pose. Slots organize attachments for {@link Skeleton#drawOrder} purposes and provide a place to store state
* for an attachment. State cannot be stored in an attachment itself because attachments are stateless and may be shared across
* multiple skeletons. */
class SlotPose implements Pose<SlotPose> {
/** The color used to tint the slot's attachment. If SlotData.darkColor is set, this is used as the light color for two
* color tinting. */
public final color:Color = new Color(1, 1, 1, 1);
/** The dark color used to tint the slot's attachment for two color tinting, or null if two color tinting is not used. The dark
* color's alpha is not used. */
public var darkColor:Color = null;
public var attachment(default, set):Attachment; // Not used in setup pose.
/** The index of the texture region to display when the slot's attachment has a spine.attachments.Sequence. -1 represents the
* Sequence.getSetupIndex(). */
public var sequenceIndex = -1;
/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
* weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
* @see spine.attachments.VertexAttachment.computeWorldVertices()
* @see spine.animation.DeformTimeline */
public var deform:Array<Float> = new Array<Float>();
public function new () {
}
public function set (pose:SlotPose):Void {
if (pose == null) throw new SpineException("pose cannot be null.");
color.setFromColor(pose.color);
if (darkColor != null) darkColor.setFromColor(pose.darkColor);
attachment = pose.attachment;
sequenceIndex = pose.sequenceIndex;
deform.resize(0);
for (e in pose.deform) deform.push(e);
}
/** Sets the slot's attachment and, if the attachment changed, resets sequenceIndex and clears the deform.
* The deform is not cleared if the old attachment has the same spine.attachments.VertexAttachment.timelineAttachment as the
* specified attachment. */
public function set_attachment(attachmentNew:Attachment):Attachment {
if (attachment == attachmentNew) return attachment;
if (!Std.isOfType(attachmentNew, VertexAttachment) || !Std.isOfType(attachment, VertexAttachment)
|| cast(attachmentNew, VertexAttachment).timelineAttachment != cast(attachment, VertexAttachment).timelineAttachment) {
deform = new Array<Float>();
}
attachment = attachmentNew;
sequenceIndex = -1;
return attachment;
}
}

View File

@ -33,272 +33,96 @@ package spine;
* bones to match that of the target bone.
*
* @see https://esotericsoftware.com/spine-transform-constraints Transform constraints in the Spine User Guide */
class TransformConstraint implements Updatable {
private var _data:TransformConstraintData;
private var _bones:Array<Bone>;
class TransformConstraint extends Constraint<TransformConstraint, TransformConstraintData, TransformConstraintPose> {
/** The bones that will be modified by this transform constraint. */
public final bones:Array<BonePose>;
/** The target bone whose world transform will be copied to the constrained bones. */
public var target:Bone;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
public var mixRotate:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
public var mixX:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
public var mixY:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
public var mixScaleX:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
public var mixScaleY:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
public var mixShearY:Float = 0;
public var source:Bone;
private var _temp:Array<Float> = new Array<Float>();
public var active:Bool = false;
/** Copy constructor. */
public function new(data:TransformConstraintData, skeleton:Skeleton) {
if (data == null)
throw new SpineException("data cannot be null.");
if (skeleton == null)
throw new SpineException("skeleton cannot be null.");
_data = data;
super(data, new TransformConstraintPose(), new TransformConstraintPose());
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
_bones = new Array<Bone>();
for (boneData in data.bones) {
_bones.push(skeleton.findBone(boneData.name));
}
target = skeleton.findBone(data.target.name);
mixRotate = data.mixRotate;
mixX = data.mixX;
mixY = data.mixY;
mixScaleX = data.mixScaleX;
mixScaleY = data.mixScaleY;
mixShearY = data.mixShearY;
bones = new Array<BonePose>();
for (boneData in data.bones)
bones.push(skeleton.bones[boneData.index].constrained);
source = skeleton.bones[data.source.index];
}
public function isActive():Bool {
return active;
}
public function setToSetupPose () {
var data:TransformConstraintData = _data;
mixRotate = data.mixRotate;
mixX = data.mixX;
mixY = data.mixY;
mixScaleX = data.mixScaleX;
mixScaleY = data.mixScaleY;
mixShearY = data.mixShearY;
public function copy(skeleton:Skeleton) {
var copy = new TransformConstraint(data, skeleton);
copy.pose.set(pose);
return copy;
}
/** Applies the constraint to the constrained bones. */
public function update(physics:Physics):Void {
if (mixRotate == 0 && mixX == 0 && mixY == 0 && mixScaleX == 0 && mixScaleY == 0 && mixShearY == 0)
return;
public function update(skeleton:Skeleton, physics:Physics):Void {
var p = applied;
if (p.mixRotate == 0 && p.mixX == 0 && p.mixY == 0 && p.mixScaleX == 0 && p.mixScaleY == 0 && p.mixShearY == 0) return;
if (data.local) {
if (data.relative) {
applyRelativeLocal();
} else {
applyAbsoluteLocal();
}
} else {
if (data.relative) {
applyRelativeWorld();
} else {
applyAbsoluteWorld();
var localSource = data.localSource, localTarget = data.localTarget, additive = data.additive, clamp = data.clamp;
var offsets = data.offsets;
var source = this.source.applied;
if (localSource) source.validateLocalTransform(skeleton);
var fromItems = data.properties;
var fn = data.properties.length, update = skeleton.update;
var bones = this.bones;
var i = 0, n = this.bones.length;
var update = skeleton._update;
while (i < n) {
var bone = bones[i];
if (localTarget)
bone.modifyLocal(skeleton);
else
bone.modifyWorld(update);
var f = 0;
while (f < fn) {
var from = fromItems[f];
var value = from.value(source, localSource, offsets) - from.offset;
var toItems = from.to;
var t = 0, tn = from.to.length;
while (t < tn) {
var to = toItems[t];
if (to.mix(p) != 0) {
var 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(p, bone, clamped, localTarget, additive);
}
t++;
}
f++;
}
i++;
}
}
private function applyAbsoluteWorld():Void {
var translate:Bool = mixX != 0 || mixY != 0;
var ta:Float = target.a,
tb:Float = target.b,
tc:Float = target.c,
td:Float = target.d;
var degRadReflect:Float = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad;
var offsetRotation:Float = data.offsetRotation * degRadReflect;
var offsetShearY:Float = data.offsetShearY * degRadReflect;
for (bone in bones) {
if (mixRotate != 0) {
var a:Float = bone.a,
b:Float = bone.b,
c:Float = bone.c,
d:Float = bone.d;
var r:Float = Math.atan2(tc, ta) - Math.atan2(c, a) + offsetRotation;
if (r > Math.PI)
r -= Math.PI * 2;
else if (r < -Math.PI)
r += Math.PI * 2;
r *= mixRotate;
var cos:Float = Math.cos(r), sin:Float = 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) {
_temp[0] = data.offsetX;
_temp[1] = data.offsetY;
target.localToWorld(_temp);
bone.worldX += (_temp[0] - bone.worldX) * mixX;
bone.worldY += (_temp[1] - bone.worldY) * mixY;
}
if (mixScaleX != 0) {
var s:Float = Math.sqrt(bone.a * bone.a + bone.c * bone.c);
if (s != 0)
s = (s + (Math.sqrt(ta * ta + tc * tc) - s + _data.offsetScaleX) * mixScaleX) / s;
bone.a *= s;
bone.c *= s;
}
if (mixScaleY != 0) {
var s:Float = Math.sqrt(bone.b * bone.b + bone.d * bone.d);
if (s != 0)
s = (s + (Math.sqrt(tb * tb + td * td) - s + _data.offsetScaleY) * mixScaleY) / s;
bone.b *= s;
bone.d *= s;
}
if (mixShearY > 0) {
var by:Float = Math.atan2(bone.d, bone.b);
var r:Float = Math.atan2(td, tb) - Math.atan2(tc, ta) - (by - Math.atan2(bone.c, bone.a));
if (r > Math.PI)
r -= Math.PI * 2;
else if (r < -Math.PI)
r += Math.PI * 2;
r = by + (r + offsetShearY) * mixShearY;
var s:Float = Math.sqrt(bone.b * bone.b + bone.d * bone.d);
bone.b = Math.cos(r) * s;
bone.d = Math.sin(r) * s;
}
bone.updateAppliedTransform();
public function sort (skeleton:Skeleton) {
if (!data.localSource) skeleton.sortBone(source);
var bones = this.bones;
var boneCount = this.bones.length;
var worldTarget = !data.localTarget;
if (worldTarget) {
for (i in 0...boneCount)
skeleton.sortBone(bones[i].bone);
}
}
public function applyRelativeWorld():Void {
var translate:Bool = mixX != 0 || mixY != 0;
var ta:Float = target.a,
tb:Float = target.b,
tc:Float = target.c,
td:Float = target.d;
var degRadReflect:Float = ta * td - tb * tc > 0 ? MathUtils.degRad : -MathUtils.degRad;
var offsetRotation:Float = _data.offsetRotation * degRadReflect,
offsetShearY:Float = _data.offsetShearY * degRadReflect;
for (bone in bones) {
if (mixRotate != 0) {
var a:Float = bone.a,
b:Float = bone.b,
c:Float = bone.c,
d:Float = bone.d;
var r:Float = Math.atan2(tc, ta) + offsetRotation;
if (r > MathUtils.PI)
r -= MathUtils.PI2;
else if (r < -MathUtils.PI)
r += MathUtils.PI2;
r *= mixRotate;
var cos:Float = Math.cos(r), sin:Float = 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) {
var temp:Array<Float> = _temp;
temp[0] = _data.offsetX;
temp[1] = _data.offsetY;
target.localToWorld(temp);
bone.worldX += temp[0] * mixX;
bone.worldY += temp[1] * mixY;
}
if (mixScaleX != 0) {
var s:Float = (Math.sqrt(ta * ta + tc * tc) - 1 + _data.offsetScaleX) * mixScaleX + 1;
bone.a *= s;
bone.c *= s;
}
if (mixScaleY != 0) {
var s:Float = (Math.sqrt(tb * tb + td * td) - 1 + _data.offsetScaleY) * mixScaleY + 1;
bone.b *= s;
bone.d *= s;
}
if (mixShearY > 0) {
var r = Math.atan2(td, tb) - Math.atan2(tc, ta);
if (r > MathUtils.PI)
r -= MathUtils.PI2;
else if (r < -MathUtils.PI)
r += MathUtils.PI2;
var b = bone.b;
var d = bone.d;
r = Math.atan2(d, b) + (r - MathUtils.PI / 2 + offsetShearY) * mixShearY;
var s = Math.sqrt(b * b + d * d);
bone.b = Math.cos(r) * s;
bone.d = Math.sin(r) * s;
}
bone.updateAppliedTransform();
skeleton._updateCache.push(this);
for (i in 0...boneCount) {
var bone = bones[i].bone;
skeleton.sortReset(bone.children);
skeleton.constrained(bone);
}
for (i in 0...boneCount)
bones[i].bone.sorted = worldTarget;
}
public function applyAbsoluteLocal():Void {
for (bone in bones) {
var rotation:Float = bone.arotation;
if (mixRotate != 0) rotation += (target.arotation - rotation + _data.offsetRotation) * mixRotate;
var x:Float = bone.ax, y:Float = bone.ay;
x += (target.ax - x + _data.offsetX) * mixX;
y += (target.ay - y + _data.offsetY) * mixY;
var scaleX:Float = bone.ascaleX, scaleY:Float = bone.ascaleY;
if (mixScaleX != 0 && scaleX != 0) {
scaleX = (scaleX + (target.ascaleX - scaleX + _data.offsetScaleX) * mixScaleX) / scaleX;
}
if (mixScaleY != 0 && scaleY != 0) {
scaleY = (scaleY + (target.ascaleY - scaleY + _data.offsetScaleY) * mixScaleX) / scaleY;
}
var shearY:Float = bone.ashearY;
if (mixShearY != 0) shearY += (target.ashearY - shearY + _data.offsetShearY) * mixShearY;
bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
}
}
public function applyRelativeLocal():Void {
for (bone in bones) {
var rotation:Float = bone.arotation + (target.arotation + _data.offsetRotation) * mixRotate;
var x:Float = bone.ax + (target.ax + _data.offsetX) * mixX;
var y:Float = bone.ay + (target.ay + _data.offsetY) * mixY;
var scaleX:Float = bone.ascaleX * (((target.ascaleX - 1 + _data.offsetScaleX) * mixScaleX) + 1);
var scaleY:Float = bone.ascaleY * (((target.ascaleY - 1 + _data.offsetScaleY) * mixScaleY) + 1);
var shearY:Float = bone.ashearY + (target.ashearY + _data.offsetShearY) * mixShearY;
bone.updateWorldTransformWith(x, y, rotation, scaleX, scaleY, bone.ashearX, shearY);
}
}
/** The transform constraint's setup pose data. */
public var data(get, never):TransformConstraintData;
private function get_data():TransformConstraintData {
return _data;
}
/** The bones that will be modified by this transform constraint. */
public var bones(get, never):Array<Bone>;
private function get_bones():Array<Bone> {
return _bones;
}
public function toString():String {
return _data.name != null ? _data.name : "TransformConstraint?";
override public function isSourceActive () {
return source.active;
}
}

View File

@ -31,27 +31,34 @@ package spine;
/**
* Stores the setup pose for a spine.TransformConstraint.
*
*
*
*
* @see https://esotericsoftware.com/spine-transform-constraints Transform constraints in the Spine User Guide
*/
class TransformConstraintData extends ConstraintData {
private var _bones:Array<BoneData> = new Array<BoneData>();
class TransformConstraintData extends ConstraintData<TransformConstraint, TransformConstraintPose> {
/** The bones that will be modified by this transform constraint. */
public final bones:Array<BoneData> = new Array<BoneData>();
/** The bone whose world transform will be copied to the constrained bones. */
public var source(default, set):BoneData;
public var offsets:Array<Float> = [for (_ in 0...6) 0.0];
/** Reads the source bone's local transform instead of its world transform. */
public var localSource = false;
/** Sets the constrained bones' local transforms instead of their world transforms. */
public var localTarget = false;
/** Adds the source bone transform to the constrained bones instead of setting it absolutely. */
public var additive = false;
/** Prevents constrained bones from exceeding the ranged defined by ToProperty.offset and ToProperty.max. */
public var clamp = false;
/** The mapping of transform properties to other transform properties. */
public final properties = new Array<FromProperty>();
/** The target bone whose world transform will be copied to the constrained bones. */
public var target:BoneData;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
public var mixRotate:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
public var mixX:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
public var mixY:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
public var mixScaleX:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
public var mixScaleY:Float = 0;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
public var mixShearY:Float = 0;
/** An offset added to the constrained bone rotation. */
public var offsetRotation:Float = 0;
/** An offset added to the constrained bone X translation. */
@ -67,16 +74,286 @@ class TransformConstraintData extends ConstraintData {
public var relative:Bool = false;
public var local:Bool = false;
public function new(name:String) {
super(name, 0, false);
public function new (name:String) {
super(name, new TransformConstraintPose());
}
/**
* The bones that will be modified by this transform constraint.
*/
public var bones(get, never):Array<BoneData>;
public function create (skeleton:Skeleton) {
return new TransformConstraint(this, skeleton);
}
private function get_bones():Array<BoneData> {
return _bones;
public function set_source (source:BoneData):BoneData {
if (source == null) throw new SpineException("source cannot be null.");
this.source = source;
return source;
}
/** An offset added to the constrained bone rotation. */
public function getOffsetRotation ():Float {
return offsets[0];
}
public function setOffsetRotation (offsetRotation:Float):Float {
offsets[0] = offsetRotation;
return offsetRotation;
}
/** An offset added to the constrained bone X translation. */
public function getOffsetX ():Float {
return offsets[1];
}
public function setOffsetX (offsetX:Float):Float {
offsets[1] = offsetX;
return offsetX;
}
/** An offset added to the constrained bone Y translation. */
public function getOffsetY ():Float {
return offsets[2];
}
public function setOffsetY (offsetY:Float):Float {
offsets[2] = offsetY;
return offsetY;
}
/** An offset added to the constrained bone scaleX. */
public function getOffsetScaleX ():Float {
return offsets[3];
}
public function setOffsetScaleX (offsetScaleX:Float):Float {
offsets[3] = offsetScaleX;
return offsetScaleX;
}
/** An offset added to the constrained bone scaleY. */
public function getOffsetScaleY ():Float {
return offsets[4];
}
public function setOffsetScaleY (offsetScaleY:Float):Float {
offsets[4] = offsetScaleY;
return offsetScaleY;
}
/** An offset added to the constrained bone shearY. */
public function getOffsetShearY ():Float {
return offsets[5];
}
public function setOffsetShearY (offsetShearY:Float):Float {
offsets[5] = offsetShearY;
return offsetShearY;
}
}
/** Source property for a {@link TransformConstraint}. */
abstract class FromProperty {
/** The value of this property that corresponds to ToProperty.offset. */
public var offset:Float;
/** Constrained properties. */
public final to = new Array<ToProperty>();
/** Reads this property from the specified bone. */
abstract public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float;
}
/** Constrained property for a TransformConstraint. */
abstract class ToProperty {
/** The value of this property that corresponds to FromProperty.offset. */
public var offset:Float;
/** The maximum value of this property when is clamped (TransformConstraintData.clamp). */
public var max:Float;
/** The scale of the FromProperty value in relation to this property. */
public var scale:Float;
/** Reads the mix for this property from the specified pose. */
abstract public function mix (pose:TransformConstraintPose):Float;
/** Applies the value to this property. */
abstract public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void;
}
class FromRotate extends FromProperty {
public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float {
if (local) return source.rotation + offsets[0];
var value = Math.atan2(source.c, source.a) * MathUtils.radDeg
+ (source.a * source.d - source.b * source.c > 0 ? offsets[0] : -offsets[0]);
if (value < 0) value += 360;
return value;
}
}
class ToRotate extends ToProperty {
public function mix (pose:TransformConstraintPose):Float {
return pose.mixRotate;
}
public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void {
if (local) {
if (!additive) value -= bone.rotation;
bone.rotation += value * pose.mixRotate;
} else {
var a = bone.a, b = bone.b, c = bone.c, d = bone.d;
value *= MathUtils.degRad;
if (!additive) value -= Math.atan2(c, a);
if (value > Math.PI)
value -= MathUtils.PI2;
else if (value < -Math.PI) //
value += MathUtils.PI2;
value *= pose.mixRotate;
var 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;
}
}
}
class FromX extends FromProperty {
public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float {
return local ? source.x + offsets[1] : offsets[1] * source.a + offsets[2] * source.b + source.worldX;
}
}
class ToX extends ToProperty {
public function mix (pose:TransformConstraintPose):Float {
return pose.mixX;
}
public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void {
if (local) {
if (!additive) value -= bone.x;
bone.x += value * pose.mixX;
} else {
if (!additive) value -= bone.worldX;
bone.worldX += value * pose.mixX;
}
}
}
class FromY extends FromProperty {
public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float {
return local ? source.y + offsets[2] : offsets[1] * source.c + offsets[2] * source.d + source.worldY;
}
}
class ToY extends ToProperty {
public function mix (pose:TransformConstraintPose):Float {
return pose.mixY;
}
public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void {
if (local) {
if (!additive) value -= bone.y;
bone.y += value * pose.mixY;
} else {
if (!additive) value -= bone.worldY;
bone.worldY += value * pose.mixY;
}
}
}
class FromScaleX extends FromProperty {
public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float {
return (local ? source.scaleX : Math.sqrt(source.a * source.a + source.c * source.c)) + offsets[3];
}
}
class ToScaleX extends ToProperty {
public function mix (pose:TransformConstraintPose):Float {
return pose.mixScaleX;
}
public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void {
if (local) {
if (additive)
bone.scaleX *= 1 + ((value - 1) * pose.mixScaleX);
else if (bone.scaleX != 0) //
bone.scaleX = 1 + (value / bone.scaleX - 1) * pose.mixScaleX;
} else {
var s:Float;
if (additive)
s = 1 + (value - 1) * pose.mixScaleX;
else {
s = Math.sqrt(bone.a * bone.a + bone.c * bone.c);
if (s != 0) s = 1 + (value / s - 1) * pose.mixScaleX;
}
bone.a *= s;
bone.c *= s;
}
}
}
class FromScaleY extends FromProperty {
public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float {
return (local ? source.scaleY : Math.sqrt(source.b * source.b + source.d * source.d)) + offsets[4];
}
}
class ToScaleY extends ToProperty {
public function mix (pose:TransformConstraintPose):Float {
return pose.mixScaleY;
}
public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void {
if (local) {
if (additive)
bone.scaleY *= 1 + ((value - 1) * pose.mixScaleY);
else if (bone.scaleY != 0) //
bone.scaleY = 1 + (value / bone.scaleY - 1) * pose.mixScaleY;
} else {
var s:Float;
if (additive)
s = 1 + (value - 1) * pose.mixScaleY;
else {
s = Math.sqrt(bone.b * bone.b + bone.d * bone.d);
if (s != 0) s = 1 + (value / s - 1) * pose.mixScaleY;
}
bone.b *= s;
bone.d *= s;
}
}
}
class FromShearY extends FromProperty {
public function value (source:BonePose, local:Bool, offsets:Array<Float>):Float {
return (local ? source.shearY : (Math.atan2(source.d, source.b) - Math.atan2(source.c, source.a)) * MathUtils.radDeg - 90) + offsets[5];
}
}
class ToShearY extends ToProperty {
public function mix (pose:TransformConstraintPose):Float {
return pose.mixShearY;
}
public function apply (pose:TransformConstraintPose, bone:BonePose, value:Float, local:Bool, additive:Bool):Void {
if (local) {
if (!additive) value -= bone.shearY;
bone.shearY += value * pose.mixShearY;
} else {
var b = bone.b, d = bone.d, by = Math.atan2(d, b);
value = (value + 90) * MathUtils.degRad;
if (additive)
value -= Math.PI / 2;
else {
value -= by - Math.atan2(bone.c, bone.a);
if (value > Math.PI)
value -= MathUtils.PI2;
else if (value < -Math.PI) //
value += MathUtils.PI2;
}
value = by + value * pose.mixShearY;
var s = Math.sqrt(b * b + d * d);
bone.b = Math.cos(value) * s;
bone.d = Math.sin(value) * s;
}
}
}

View File

@ -0,0 +1,65 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine;
/** Stores a pose for a transform constraint. */
class TransformConstraintPose implements Pose<TransformConstraintPose> {
/** A percentage (0-1) that controls the mix between the constrained and unconstrained rotation. */
public var mixRotate = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation X. */
public var mixX = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained translation Y. */
public var mixY = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale X. */
public var mixScaleX = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained scale Y. */
public var mixScaleY = 0.;
/** A percentage (0-1) that controls the mix between the constrained and unconstrained shear Y. */
public var mixShearY = 0.;
public function new () {
}
public function set (pose:TransformConstraintPose) {
mixRotate = pose.mixRotate;
mixX = pose.mixX;
mixY = pose.mixY;
mixScaleX = pose.mixScaleX;
mixScaleY = pose.mixScaleY;
mixShearY = pose.mixShearY;
}
}

View File

@ -30,16 +30,7 @@
package spine;
/** The interface for items updated by spine.Skeleton.updateWorldTransform(). */
interface Updatable {
interface Update {
/** @param physics Determines how physics and other non-deterministic updates are applied. */
function update(physics:Physics):Void;
/** Returns false when this item won't be updated by
* spine.Skeleton.updateWorldTransform() because a skin is required and the
* active skin does not contain this item.
* @see spine.Skin.getBones()
* @see spine.Skin.getConstraints()
* @see spine.BoneData.getSkinRequired()
* @see spine.ConstraintData.getSkinRequired() */
function isActive():Bool;
function update(skeleton:Skeleton, physics:Physics):Void;
}

View File

@ -43,7 +43,7 @@ class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
private var slotIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, slotIndex:Int) {
super(frameCount, bezierCount, [Property.alpha + "|" + slotIndex]);
super(frameCount, bezierCount, Property.alpha + "|" + slotIndex);
this.slotIndex = slotIndex;
}
@ -55,15 +55,15 @@ class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
return slotIndex;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var color:Color = slot.color;
var slot = skeleton.slots[slotIndex];
if (!slot.bone.active) return;
var color = (appliedPose ? slot.applied : slot.pose).color;
if (time < frames[0]) {
var setup:Color = slot.data.color;
var setup:Color = slot.data.setup.color;
switch (blend) {
case MixBlend.setup:
color.a = setup.a;
@ -78,7 +78,7 @@ class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
color.a = a;
} else {
if (blend == MixBlend.setup)
color.a = slot.data.color.a;
color.a = slot.data.setup.color.a;
color.a += (a - color.a) * alpha;
}
}

View File

@ -29,37 +29,52 @@
package spine.animation;
import haxe.ds.IntMap;
import haxe.ds.StringMap;
import spine.Event;
import spine.Skeleton;
/** Stores a list of timelines to animate a skeleton's pose over time. */
class Animation {
private var _name:String;
private var _timelines:Array<Timeline>;
private var _timelineIds:StringMap<Bool> = new StringMap<Bool>();
/** The animation's name, which is unique across all animations in the skeleton. */
public final name:String;
/** The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is
* used to know when it has completed and when it should loop back to the start. */
* used to know when it has completed and when it should loop back to the start. */
public var duration:Float = 0;
public var timelines:Array<Timeline>;
public final timelineIds:StringMap<Bool> = new StringMap<Bool>();
public final bones:Array<Int>;
public function new(name:String, timelines:Array<Timeline>, duration:Float) {
if (name == null)
throw new SpineException("name cannot be null.");
_name = name;
setTimelines(timelines);
if (name == null) throw new SpineException("name cannot be null.");
this.name = name;
this.duration = duration;
var n = timelines.length << 1;
timelineIds = new StringMap<Bool>();
bones = new Array<Int>();
setTimelines(timelines);
}
public function setTimelines(timelines:Array<Timeline>) {
if (timelines == null)
throw new SpineException("timelines cannot be null.");
_timelines = timelines;
_timelineIds = new StringMap<Bool>();
if (timelines == null) throw new SpineException("timelines cannot be null.");
this.timelines = timelines;
timelineIds.clear();
bones.resize(0);
var boneSet = new IntMap<Bool>();
for (timeline in timelines) {
var ids:Array<String> = timeline.propertyIds;
for (id in ids) {
_timelineIds.set(id, true);
for (id in ids) timelineIds.set(id, true);
if (Std.isOfType(timeline, BoneTimeline)) {
var boneTimeline = cast(timeline, BoneTimeline);
var boneIndex = boneTimeline.getBoneIndex();
if (!boneSet.exists(boneIndex)) {
boneSet.set(boneIndex, true);
bones.push(boneIndex);
}
}
}
}
@ -67,14 +82,14 @@ class Animation {
/** Returns true if this animation contains a timeline with any of the specified property IDs. */
public function hasTimeline(ids:Array<String>):Bool {
for (id in ids) {
if (_timelineIds.exists(id))
if (timelineIds.exists(id))
return true;
}
return false;
}
/** Applies the animation's timelines to the specified skeleton.
*
*
* See Timeline.apply().
* @param skeleton The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton
* components the timelines may change.
@ -94,36 +109,16 @@ class Animation {
* @param direction Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions,
* such as DrawOrderTimeline or AttachmentTimeline. */
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, loop:Bool, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
if (skeleton == null)
throw new SpineException("skeleton cannot be null.");
direction:MixDirection, appliedPose:Bool):Void {
if (skeleton == null) throw new SpineException("skeleton cannot be null.");
if (loop && duration != 0) {
time %= duration;
if (lastTime > 0)
lastTime %= duration;
if (lastTime > 0) lastTime %= duration;
}
for (timeline in timelines) {
timeline.apply(skeleton, lastTime, time, events, alpha, blend, direction);
timeline.apply(skeleton, lastTime, time, events, alpha, blend, direction, appliedPose);
}
}
/** The animation's name, which is unique across all animations in the skeleton. */
public var name(get, never):String;
private function get_name():String {
return _name;
}
public function toString():String {
return _name;
}
/** If the returned array or the timelines it contains are modified, setTimelines() must be called. */
public var timelines(get, never):Array<Timeline>;
private function get_timelines():Array<Timeline> {
return _timelines;
}
}

View File

@ -298,7 +298,7 @@ class AnimationState {
for (slot in skeleton.slots) {
if (slot.attachmentState == setupState) {
var attachmentName:String = slot.data.attachmentName;
slot.attachment = attachmentName == null ? null : skeleton.getAttachmentForSlotIndex(slot.data.index, attachmentName);
slot.pose.attachment = attachmentName == null ? null : skeleton.getAttachmentForSlotIndex(slot.data.index, attachmentName);
}
}
unkeyedState += 2; // Increasing after each use avoids the need to reset attachmentState for every slot.
@ -437,28 +437,29 @@ class AnimationState {
timelinesRotation[i] = 0;
if (alpha == 1) {
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn);
timeline.apply(skeleton, 0, time, null, 1, blend, MixDirection.mixIn, false);
return;
}
var bone = skeleton.bones[timeline.boneIndex];
if (!bone.active)
return;
var pose = bone.pose, setup = bone.data.setup;
var frames = timeline.frames;
var r1:Float = 0, r2:Float = 0;
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
bone.rotation = bone.data.rotation;
pose.rotation = setup.rotation;
default:
return;
case MixBlend.first:
r1 = bone.rotation;
r2 = bone.data.rotation;
r1 = pose.rotation;
r2 = setup.rotation;
}
} else {
r1 = blend == MixBlend.setup ? bone.data.rotation : bone.rotation;
r2 = bone.data.rotation + timeline.getCurveValue(time);
r1 = blend == MixBlend.setup ? setup.rotation : pose.rotation;
r2 = setup.rotation + timeline.getCurveValue(time);
}
// Mix between rotations using the direction of the shortest route on the first frame while detecting crosses.
@ -492,11 +493,11 @@ class AnimationState {
timelinesRotation[i] = total;
}
timelinesRotation[i + 1] = diff;
bone.rotation = r1 + total * alpha;
pose.rotation = r1 + total * alpha;
}
private function setAttachment(skeleton:Skeleton, slot:Slot, attachmentName:String, attachments:Bool):Void {
slot.attachment = attachmentName == null ? null : skeleton.getAttachmentForSlotIndex(slot.data.index, attachmentName);
slot.pose.attachment = attachmentName == null ? null : skeleton.getAttachmentForSlotIndex(slot.data.index, attachmentName);
if (attachments)
slot.attachmentState = unkeyedState + CURRENT;
}
@ -629,9 +630,9 @@ class AnimationState {
return setAnimation(trackIndex, animation, loop);
}
/**
* Sets the current animation for a track, discarding any queued animations. If the formerly current track entry was never
* applied to a skeleton, it is replaced (not mixed from).
/** Sets the current animation for a track, discarding any queued animations.
* If the formerly current track entry is for the same animation and was never applied to a skeleton, it is replaced (not mixed
* from).
* @param loop If true, the animation will repeat. If false it will not, instead its last frame is applied if played beyond its
* duration. In either case spine.animation.TrackEntry.getTrackEnd() determines when the track is cleared.
* @return A track entry to allow further customization of animation playback. References to the track entry must not be kept
@ -643,7 +644,7 @@ class AnimationState {
var interrupt:Bool = true;
var current:TrackEntry = expandToIndex(trackIndex);
if (current != null) {
if (current.nextTrackLast == -1) {
if (current.nextTrackLast == -1 && current.animation == animation) {
// Don't mix from an entry that was never applied.
tracks[trackIndex] = current.mixingFrom;
queue.interrupt(current);
@ -673,8 +674,7 @@ class AnimationState {
return addAnimation(trackIndex, animation, loop, delay);
}
/**
* Adds an animation to be played after the current or last queued animation for a track. If the track is empty, it is
/** Adds an animation to be played after the current or last queued animation for a track. If the track has no entries, this is
* equivalent to calling spine.animation.AnimationState.setAnimation().
* @param delay If > 0, sets spine.animation.TrackEntry.getDelay(). If <= 0, the delay set is the duration of the previous track entry
* minus any mix duration (from the spine.animation.AnimationStateData) plus the specified delay (ie the mix
@ -726,7 +726,9 @@ class AnimationState {
* animation to be applied more and more over the mix duration. Properties keyed in the new animation transition from the value
* from lower tracks or from the setup pose value if no lower tracks key the property to the value keyed in the new
* animation.
*/
*
* See <a href='https://esotericsoftware.com/spine-applying-animations/#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide. */
public function setEmptyAnimation(trackIndex:Int, mixDuration:Float):TrackEntry {
var entry:TrackEntry = setAnimation(trackIndex, emptyAnimation, false);
entry.mixDuration = mixDuration;
@ -736,10 +738,12 @@ class AnimationState {
/**
* Adds an empty animation to be played after the current or last queued animation for a track, and sets the track entry's
* spine.animation.TrackEntry.getMixDuration(). If the track is empty, it is equivalent to calling
* spine.animation.TrackEntry.getMixDuration(). If the track has no entries,, it is equivalent to calling
* spine.animation.AnimationState.setEmptyAnimation().
*
* See spine.animation.AnimationState.setEmptyAnimation().
* See spine.animation.AnimationState.setEmptyAnimation() and
* <a href='https://esotericsoftware.com/spine-applying-animations/#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide.
* @param delay If > 0, sets spine.animation.TrackEntry.getDelay(). If <= 0, the delay set is the duration of the previous track entry
* minus any mix duration plus the specified delay (ie the mix ends at (delay = 0) or
* before (delay < 0) the previous track entry duration). If the previous entry is looping, its next
@ -755,10 +759,10 @@ class AnimationState {
return entry;
}
/**
* Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix
* duration.
*/
/** Sets an empty animation for every track, discarding any queued animations, and mixes to it over the specified mix duration.
*
* See <a href='https://esotericsoftware.com/spine-applying-animations/#Empty-animations'>Empty animations</a> in the Spine
* Runtimes Guide. */
public function setEmptyAnimations(mixDuration:Float):Void {
var oldDrainDisabled:Bool = queue.drainDisabled;
queue.drainDisabled = true;

View File

@ -41,7 +41,7 @@ class AttachmentTimeline extends Timeline implements SlotTimeline {
public var attachmentNames:Array<String>;
public function new(frameCount:Int, slotIndex:Int) {
super(frameCount, [Property.attachment + "|" + slotIndex]);
super(frameCount, Property.attachment + "|" + slotIndex);
this.slotIndex = slotIndex;
attachmentNames = new Array<String>();
attachmentNames.resize(frameCount);
@ -63,30 +63,22 @@ class AttachmentTimeline extends Timeline implements SlotTimeline {
attachmentNames[frame] = attachmentName;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var slot = skeleton.slots[slotIndex];
if (!slot.bone.active) return;
var pose = appliedPose ? slot.applied : slot.pose;
if (direction == MixDirection.mixOut) {
if (blend == MixBlend.setup) {
setAttachment(skeleton, slot, slot.data.attachmentName);
}
return;
}
if (time < frames[0]) {
if (blend == MixBlend.setup || blend == MixBlend.first) {
setAttachment(skeleton, slot, slot.data.attachmentName);
}
return;
}
setAttachment(skeleton, slot, attachmentNames[Timeline.search1(frames, time)]);
if (blend == MixBlend.setup) setAttachment(skeleton, pose, slot.data.attachmentName);
} else if (time < frames[0]) {
if (blend == MixBlend.setup || blend == MixBlend.first) setAttachment(skeleton, pose, slot.data.attachmentName);
} else
setAttachment(skeleton, pose, attachmentNames[Timeline.search1(frames, time)]);
}
private function setAttachment(skeleton:Skeleton, slot:Slot, attachmentName:String):Void {
slot.attachment = attachmentName == null ? null : skeleton.getAttachmentForSlotIndex(slotIndex, attachmentName);
private function setAttachment(skeleton:Skeleton, pose:SlotPose, attachmentName:String):Void {
pose.attachment = attachmentName == null ? null : skeleton.getAttachmentForSlotIndex(slotIndex, attachmentName);
}
}

View File

@ -32,5 +32,5 @@ package spine.animation;
/** An interface for timelines which change the property of a bone. */
interface BoneTimeline {
/** The index of the bone in spine.Skeleton.getBones() that will be changed when this timeline is applied. */
function getBoneIndex():Int;
public function getBoneIndex():Int;
}

View File

@ -0,0 +1,52 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.animation;
abstract class BoneTimeline1 extends CurveTimeline1 implements BoneTimeline {
public final boneIndex:Int;
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int, property:Property) {
super(frameCount, bezierCount, property + "|" + boneIndex);
this.boneIndex = boneIndex;
}
public function getBoneIndex () {
return boneIndex;
}
public function apply (skeleton: Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection, appliedPose:Bool) {
var bone = skeleton.bones[boneIndex];
if (bone.active) apply1(appliedPose ? bone.applied : bone.pose, bone.data.setup, time, alpha, blend, direction);
}
abstract function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection):Void;
}

View File

@ -29,17 +29,18 @@
package spine.animation;
/** The base class for a spine.animation.CurveTimeline which sets two properties. */
class CurveTimeline2 extends CurveTimeline {
/** The base class for a spine.animation.CurveTimeline that is a spine.animation.BoneTimeline and sets two properties. */
abstract class BoneTimeline2 extends CurveTimeline implements BoneTimeline {
private static inline var ENTRIES:Int = 3;
private static inline var VALUE1:Int = 1;
private static inline var VALUE2:Int = 2;
/** @param frameCount The number of frames in the timeline.
* @param bezierCount The maximum number of Bezier curves. See spine.animation.CurveTimeline.shrink().
* @param propertyIds Array of unique identifiers for the properties the timeline modifies. */
public function new(frameCount:Int, bezierCount:Int, propertyIds:Array<String>) {
super(frameCount, bezierCount, propertyIds);
public final boneIndex:Int;
/** @param bezierCount The maximum number of Bezier curves. See spine.animation.CurveTimeline.shrink(). */
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int, property1:Property, property2:Property) {
super(frameCount, bezierCount, property1 + "|" + boneIndex, property2 + "|" + boneIndex);
this.boneIndex = boneIndex;
}
public override function getFrameEntries():Int {
@ -55,4 +56,17 @@ class CurveTimeline2 extends CurveTimeline {
frames[frame + VALUE1] = value1;
frames[frame + VALUE2] = value2;
}
public function getBoneIndex () {
return boneIndex;
}
public function apply (skeleton: Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection, appliedPose:Bool) {
var bone = skeleton.bones[boneIndex];
if (bone.active) apply1(appliedPose ? bone.applied : bone.pose, bone.data.setup, time, alpha, blend, direction);
}
abstract function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection):Void;
}

View File

@ -0,0 +1,35 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.animation;
interface ConstraintTimeline {
/** The index of the constraint in spine.Skeleton.constraints that will be changed when this timeline is applied. */
public function getConstraintIndex():Int;
}

View File

@ -0,0 +1,43 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.animation;
abstract class ConstraintTimeline1 extends CurveTimeline1 implements ConstraintTimeline {
public final constraintIndex:Int;
public function new (frameCount:Int, bezierCount:Int, constraintIndex:Int, property:Property) {
super(frameCount, bezierCount, property + "|" + constraintIndex);
this.constraintIndex = constraintIndex;
}
public function getConstraintIndex () {
return constraintIndex;
}
}

View File

@ -30,7 +30,7 @@
package spine.animation;
/** The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. */
class CurveTimeline extends Timeline {
abstract class CurveTimeline extends Timeline {
private static inline var LINEAR:Int = 0;
private static inline var STEPPED:Int = 1;
private static inline var BEZIER:Int = 2;
@ -40,8 +40,8 @@ class CurveTimeline extends Timeline {
/** @param bezierCount The maximum number of Bezier curves. See CurveTimeline.shrink().
* @param propertyIds Unique identifiers for the properties the timeline modifies. */
public function new(frameCount:Int, bezierCount:Int, propertyIds:Array<String>) {
super(frameCount, propertyIds);
public function new(frameCount:Int, bezierCount:Int, propertyIds:...String) {
super(frameCount, ...propertyIds);
curves = new Array<Float>();
curves.resize(frameCount + bezierCount * BEZIER_SIZE);
curves[frameCount - 1] = STEPPED;

View File

@ -30,15 +30,15 @@
package spine.animation;
/** The base class for a spine.animation.CurveTimeline that sets one property. */
class CurveTimeline1 extends CurveTimeline {
abstract class CurveTimeline1 extends CurveTimeline {
private static inline var ENTRIES:Int = 2;
private static inline var VALUE:Int = 1;
/** @param frameCount The number of frames in the timeline.
* @param bezierCount The maximum number of Bezier curves. See spine.animation.CurveTimeline.shrink().
* @param propertyIds Unique identifiers for the properties the timeline modifies. */
public function new(frameCount:Int, bezierCount:Int, propertyIds:Array<String>) {
super(frameCount, bezierCount, propertyIds);
public function new(frameCount:Int, bezierCount:Int, propertyId:String) {
super(frameCount, bezierCount, propertyId);
}
public override function getFrameEntries():Int {
@ -73,75 +73,69 @@ class CurveTimeline1 extends CurveTimeline {
return value + (time - before) / (frames[i + ENTRIES] - before) * (frames[i + ENTRIES + VALUE] - value);
case CurveTimeline.STEPPED:
return frames[i + VALUE];
default:
return getBezierValue(time, i, VALUE, curveType - CurveTimeline.BEZIER);
}
return getBezierValue(time, i, VALUE, curveType - CurveTimeline.BEZIER);
}
public function getRelativeValue (time:Float, alpha:Float, blend: MixBlend, current:Float, setup:Float):Float {
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
return setup;
case MixBlend.first:
return current + (setup - current) * alpha;
case MixBlend.setup: return setup;
case MixBlend.first: return current + (setup - current) * alpha;
default: return current;
}
return current;
}
var value:Float = getCurveValue(time);
switch (blend) {
case MixBlend.setup:
return setup + value * alpha;
case MixBlend.first, MixBlend.replace:
value += setup - current;
case MixBlend.setup: return setup + value * alpha;
case MixBlend.first, MixBlend.replace: return current + (value + setup - current) * alpha;
default: return current + value * alpha; // MixBlend.add
}
return current + value * alpha;
}
public function getAbsoluteValue (time:Float, alpha:Float, blend: MixBlend, current:Float, setup:Float):Float {
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
return setup;
case MixBlend.first:
return current + (setup - current) * alpha;
case MixBlend.setup: return setup;
case MixBlend.first: return current + (setup - current) * alpha;
default: return current;
}
return current;
}
var value:Float = getCurveValue(time);
if (blend == MixBlend.setup) return setup + (value - setup) * alpha;
return current + (value - current) * alpha;
switch (blend) {
case MixBlend.setup: return setup + (value - setup) * alpha;
case MixBlend.first, MixBlend.replace: return current + (value - current) * alpha;
default: return current + value * alpha; // MixBlend.add
}
}
public function getAbsoluteValue2 (time:Float, alpha:Float, blend: MixBlend, current:Float, setup:Float, value:Float):Float {
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
return setup;
case MixBlend.first:
return current + (setup - current) * alpha;
case MixBlend.setup: return setup;
case MixBlend.first: return current + (setup - current) * alpha;
default: current;
}
return current;
}
if (blend == MixBlend.setup) return setup + (value - setup) * alpha;
return current + (value - current) * alpha;
switch (blend) {
case MixBlend.setup: return setup + (value - setup) * alpha;
case MixBlend.first, MixBlend.replace: return current + (value - current) * alpha;
default: return current + value * alpha; // MixBlend.add
}
}
public function getScaleValue (time:Float, alpha:Float, blend: MixBlend, direction: MixDirection, current:Float, setup:Float):Float {
var frames:Array<Float> = frames;
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
return setup;
case MixBlend.first:
return current + (setup - current) * alpha;
case MixBlend.setup: return setup;
case MixBlend.first: return current + (setup - current) * alpha;
default: return current;
}
return current;
}
var value:Float = getCurveValue(time) * setup;
if (alpha == 1) {
if (blend == MixBlend.add) return current + value - setup;
return value;
}
if (alpha == 1) return blend == MixBlend.add ? current + value - setup : value;
// Mixing out uses sign of setup or current pose, else use sign of key.
if (direction == MixDirection.mixOut) {
switch (blend) {

View File

@ -37,19 +37,17 @@ import spine.Skeleton;
import spine.Slot;
/** Changes a slot's spine.Slot.deform to deform a spine.attachments.VertexAttachment. */
class DeformTimeline extends CurveTimeline implements SlotTimeline {
public var slotIndex:Int = 0;
class DeformTimeline extends SlotCurveTimeline {
/** The attachment that will be deformed.
*
*
* @see spine.attachments.VertexAttachment.getTimelineAttachment() */
public var attachment:VertexAttachment;
public final attachment:VertexAttachment;
/** The vertices for each frame. */
public var vertices:Array<Array<Float>>;
public final vertices:Array<Array<Float>>;
public function new(frameCount:Int, bezierCount:Int, slotIndex:Int, attachment:VertexAttachment) {
super(frameCount, bezierCount, [Property.deform + "|" + slotIndex + "|" + attachment.id]);
super(frameCount, bezierCount, slotIndex, Property.deform + "|" + slotIndex + "|" + attachment.id);
this.slotIndex = slotIndex;
this.attachment = attachment;
vertices = new Array<Array<Float>>();
@ -60,10 +58,6 @@ class DeformTimeline extends CurveTimeline implements SlotTimeline {
return frames.length;
}
public function getSlotIndex():Int {
return slotIndex;
}
/** Sets the time and vertices for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds.
@ -145,110 +139,79 @@ class DeformTimeline extends CurveTimeline implements SlotTimeline {
return y + (1 - y) * (time - x) / (frames[frame + getFrameEntries()] - x);
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
var slotAttachment:Attachment = slot.attachment;
if (slotAttachment == null)
return;
if (!Std.isOfType(slotAttachment, VertexAttachment) || cast(slotAttachment, VertexAttachment).timelineAttachment != attachment)
return;
public function apply1 (slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
if (!Std.isOfType(pose.attachment, VertexAttachment)) return;
var vertexAttachment = cast(pose.attachment, VertexAttachment);
if (vertexAttachment.timelineAttachment != attachment) return;
var deform:Array<Float> = slot.deform;
if (deform.length == 0)
blend = MixBlend.setup;
var deform = pose.deform;
if (deform.length == 0) blend = MixBlend.setup;
var vertexCount:Int = vertices[0].length;
var i:Int, setupVertices:Array<Float>;
var vertexCount = vertices[0].length;
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
deform.resize(0);
case MixBlend.setup: deform.resize(0);
case MixBlend.first:
if (alpha == 1) {
deform.resize(0);
return;
}
ArrayUtils.resize(deform, vertexCount, 0);
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
// Unweighted vertex positions.
setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
if (vertexAttachment.bones == null) { // Unweighted vertex positions.
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount)
deform[i] += (setupVertices[i] - deform[i]) * alpha;
}
} else {
// Weighted deform offsets.
} else { // Weighted deform offsets.
alpha = 1 - alpha;
for (i in 0...vertexCount) {
for (i in 0...vertexCount)
deform[i] *= alpha;
}
}
}
return;
}
ArrayUtils.resize(deform, vertexCount, 0);
var setup:Float;
if (time >= frames[frames.length - 1]) {
if (time >= frames[frames.length - 1]) { // Time is after last frame.
var lastVertices:Array<Float> = vertices[frames.length - 1];
if (alpha == 1) {
if (blend == MixBlend.add) {
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
// Unweighted vertex positions, with alpha.
setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
if (vertexAttachment.bones == null) { // Unweighted vertex positions, no alpha.
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount)
deform[i] += lastVertices[i] - setupVertices[i];
}
} else {
// Weighted deform offsets, with alpha.
for (i in 0...vertexCount) {
} else { // Weighted deform offsets, with alpha.
for (i in 0...vertexCount)
deform[i] += lastVertices[i];
}
}
} else {
for (i in 0...vertexCount) {
} else
for (i in 0...vertexCount)
deform[i] = lastVertices[i];
}
}
} else {
switch (blend) {
case MixBlend.setup:
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
// Unweighted vertex positions, with alpha.
setupVertices = vertexAttachment.vertices;
if (vertexAttachment.bones == null) { // Unweighted vertex positions, with alpha.
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
setup = setupVertices[i];
var setup = setupVertices[i];
deform[i] = setup + (lastVertices[i] - setup) * alpha;
}
} else {
// Weighted deform offsets, with alpha.
for (i in 0...vertexCount) {
} else { // Weighted deform offsets, with alpha.
for (i in 0...vertexCount)
deform[i] = lastVertices[i] * alpha;
}
}
case MixBlend.first, MixBlend.replace:
for (i in 0...vertexCount) {
case MixBlend.first, MixBlend.replace: // Vertex positions or deform offsets, with alpha.
for (i in 0...vertexCount)
deform[i] += (lastVertices[i] - deform[i]) * alpha;
}
case MixBlend.add:
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
// Unweighted vertex positions, with alpha.
setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
if (vertexAttachment.bones == null) { // Unweighted vertex positions, no alpha.
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount)
deform[i] += (lastVertices[i] - setupVertices[i]) * alpha;
}
} else {
// Weighted deform offsets, with alpha.
for (i in 0...vertexCount) {
} else { // Weighted deform offsets, alpha.
for (i in 0...vertexCount)
deform[i] += lastVertices[i] * alpha;
}
}
}
}
@ -258,69 +221,63 @@ class DeformTimeline extends CurveTimeline implements SlotTimeline {
// Interpolate between the previous frame and the current frame.
var frame:Int = Timeline.search1(frames, time);
var percent:Float = getCurvePercent(time, frame);
var prevVertices:Array<Float> = vertices[frame], prev:Float;
var prevVertices:Array<Float> = vertices[frame];
var nextVertices:Array<Float> = vertices[frame + 1];
if (alpha == 1) {
if (blend == MixBlend.add) {
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
// Unweighted vertex positions, with alpha.
setupVertices = vertexAttachment.vertices;
if (vertexAttachment.bones == null) { // Unweighted vertex positions, no alpha.
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i];
}
} else {
// Weighted deform offsets, with alpha.
} else { // Weighted deform offsets, no alpha.
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] += prev + (nextVertices[i] - prev) * percent;
}
}
} else if (percent == 0) {
for (i in 0...vertexCount)
deform[i] = prevVertices[i];
} else {
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] = prev + (nextVertices[i] - prev) * percent;
}
}
} else {
switch (blend) {
case MixBlend.setup:
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
// Unweighted vertex positions, with alpha.
setupVertices = vertexAttachment.vertices;
if (vertexAttachment.bones == null) { // Unweighted vertex positions, with alpha.
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
prev = prevVertices[i];
setup = setupVertices[i];
var prev = prevVertices[i], setup = setupVertices[i];
deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha;
}
} else {
// Weighted deform offsets, with alpha.
} else { // Weighted deform offsets, with alpha.
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha;
}
}
case MixBlend.first, MixBlend.replace:
case MixBlend.first, MixBlend.replace: // Vertex positions or deform offsets, with alpha.
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha;
}
case MixBlend.add:
var vertexAttachment:VertexAttachment = cast(slotAttachment, VertexAttachment);
if (vertexAttachment.bones == null) {
if (vertexAttachment.bones == null) { // Unweighted vertex positions, with alpha.
// Unweighted vertex positions, with alpha.
setupVertices = vertexAttachment.vertices;
var setupVertices = vertexAttachment.vertices;
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha;
}
} else {
// Weighted deform offsets, with alpha.
} else { // Weighted deform offsets, with alpha.
for (i in 0...vertexCount) {
prev = prevVertices[i];
var prev = prevVertices[i];
deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha;
}
}

View File

@ -39,7 +39,7 @@ class DrawOrderTimeline extends Timeline {
public var drawOrders:Array<Array<Int>>;
public function new(frameCount:Int) {
super(frameCount, [Std.string(Property.drawOrder)]);
super(frameCount, Property.drawOrder);
drawOrders = new Array<Array<Int>>();
drawOrders.resize(frameCount);
}
@ -59,8 +59,9 @@ class DrawOrderTimeline extends Timeline {
drawOrders[frame] = drawOrder;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var drawOrder:Array<Slot> = skeleton.drawOrder;
var slots:Array<Slot> = skeleton.slots;
var i:Int = 0, n:Int = slots.length;

View File

@ -79,8 +79,7 @@ class EventQueue {
}
public function drain():Void {
if (drainDisabled)
return; // Not reentrant.
if (drainDisabled) return; // Not reentrant.
drainDisabled = true;
var i:Int = 0;

View File

@ -39,7 +39,7 @@ class EventTimeline extends Timeline {
public var events:Array<Event>;
public function new(frameCount:Int) {
super(frameCount, [Std.string(Property.event)]);
super(frameCount, Property.event);
events = new Array<Event>();
events.resize(frameCount);
}
@ -56,22 +56,17 @@ class EventTimeline extends Timeline {
}
/** Fires events for frames > lastTime and <= time. */
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
if (events == null)
return;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, firedEvents:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
if (firedEvents == null) return;
var frameCount:Int = frames.length;
if (lastTime > time) // Apply events after lastTime for looped animations.
{
apply(skeleton, lastTime, 2147483647, events, alpha, blend, direction);
if (lastTime > time) { // Apply events after lastTime for looped animations.
apply(skeleton, lastTime, 2147483647, firedEvents, alpha, blend, direction, appliedPose);
lastTime = -1;
} else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame.
{
return;
}
if (time < frames[0]) return;
var frame:Int;
@ -87,7 +82,7 @@ class EventTimeline extends Timeline {
}
}
while (i < frameCount && time >= frames[i]) {
events.push(this.events[i]);
firedEvents.push(this.events[i]);
i++;
}
}

View File

@ -29,13 +29,9 @@
package spine.animation;
import spine.Event;
import spine.IkConstraint;
import spine.Skeleton;
/** Changes an IK constraint's spine.IkConstraint.mix, spine.IkConstraint.softness,
* spine.IkConstraint.bendDirection, spine.IkConstraint.stretch, and spine.IkConstraint.compress. */
class IkConstraintTimeline extends CurveTimeline {
/** Changes an IK constraint's spine.IkConstraintPose.mix, spine.IkConstraintPose.softness,
* spine.IkConstraintPose.bendDirection, spine.IkConstraintPose.stretch, and spine.IkConstraintPose.compress. */
class IkConstraintTimeline extends CurveTimeline implements ConstraintTimeline {
private static inline var ENTRIES:Int = 6;
private static inline var MIX:Int = 1;
private static inline var SOFTNESS:Int = 2;
@ -47,15 +43,19 @@ class IkConstraintTimeline extends CurveTimeline {
* applied. */
public var constraintIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, ikConstraintIndex:Int) {
super(frameCount, bezierCount, [Property.ikConstraint + "|" + ikConstraintIndex]);
this.constraintIndex = ikConstraintIndex;
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, Property.ikConstraint + "|" + constraintIndex);
this.constraintIndex = constraintIndex;
}
public override function getFrameEntries():Int {
return ENTRIES;
}
public function getConstraintIndex () {
return constraintIndex;
}
/** Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds.
@ -70,26 +70,28 @@ class IkConstraintTimeline extends CurveTimeline {
frames[frame + STRETCH] = stretch ? 1 : 0;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var constraint:IkConstraint = skeleton.ikConstraints[constraintIndex];
if (!constraint.active)
return;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], IkConstraint);
if (!constraint.active) return;
var pose = appliedPose ? constraint.applied : constraint.pose;
if (time < frames[0]) {
var setup = constraint.data.setup;
switch (blend) {
case MixBlend.setup:
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;
pose.mix = setup.mix;
pose.softness = setup.softness;
pose.bendDirection = setup.bendDirection;
pose.compress = setup.compress;
pose.stretch = setup.stretch;
case MixBlend.first:
constraint.mix += (constraint.data.mix - constraint.mix) * alpha;
constraint.softness += (constraint.data.softness - constraint.softness) * alpha;
constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch;
pose.mix += (setup.mix - pose.mix) * alpha;
pose.softness += (setup.softness - pose.softness) * alpha;
pose.bendDirection = setup.bendDirection;
pose.compress = setup.compress;
pose.stretch = setup.stretch;
}
return;
}
@ -113,26 +115,29 @@ class IkConstraintTimeline extends CurveTimeline {
softness = getBezierValue(time, i, SOFTNESS, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
}
if (blend == MixBlend.setup) {
constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha;
constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha;
if (direction == MixDirection.mixOut) {
constraint.bendDirection = constraint.data.bendDirection;
constraint.compress = constraint.data.compress;
constraint.stretch = constraint.data.stretch;
} else {
constraint.bendDirection = Std.int(frames[i + BEND_DIRECTION]);
constraint.compress = frames[i + COMPRESS] != 0;
constraint.stretch = frames[i + STRETCH] != 0;
}
} else {
constraint.mix += (mix - constraint.mix) * alpha;
constraint.softness += (softness - constraint.softness) * alpha;
if (direction == MixDirection.mixIn) {
constraint.bendDirection = Std.int(frames[i + BEND_DIRECTION]);
constraint.compress = frames[i + COMPRESS] != 0;
constraint.stretch = frames[i + STRETCH] != 0;
}
switch (blend) {
case MixBlend.setup:
var setup = constraint.data.setup;
pose.mix = setup.mix + (mix - setup.mix) * alpha;
pose.softness = setup.softness + (softness - setup.softness) * alpha;
if (direction == MixDirection.mixOut) {
pose.bendDirection = setup.bendDirection;
pose.compress = setup.compress;
pose.stretch = setup.stretch;
return;
}
case MixBlend.first, MixBlend.replace:
pose.mix += (mix - pose.mix) * alpha;
pose.softness += (softness - pose.softness) * alpha;
if (direction == MixDirection.mixOut) return;
case MixBlend.add:
pose.mix += mix * alpha;
pose.softness += softness * alpha;
if (direction == MixDirection.mixOut) return;
}
pose.bendDirection = Std.int(frames[i + BEND_DIRECTION]);
pose.compress = frames[i + COMPRESS] != 0;
pose.stretch = frames[i + STRETCH] != 0;
}
}

View File

@ -41,18 +41,18 @@ class InheritTimeline extends Timeline implements BoneTimeline {
private var boneIndex:Int = 0;
public function new(frameCount:Int, boneIndex:Int) {
super(frameCount, [Property.inherit + "|" + boneIndex]);
super(frameCount, Property.inherit + "|" + boneIndex);
this.boneIndex = boneIndex;
}
public function getBoneIndex () {
return boneIndex;
}
public override function getFrameEntries():Int {
return ENTRIES;
}
public function getBoneIndex():Int {
return boneIndex;
}
/** Sets the transform mode for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -62,21 +62,22 @@ class InheritTimeline extends Timeline implements BoneTimeline {
frames[frame + INHERIT] = inherit.ordinal;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (!bone.active) return;
var pose = appliedPose ? bone.applied : bone.pose;
if (direction == MixDirection.mixOut) {
if (blend == MixBlend.setup) bone.inherit = bone.data.inherit;
if (blend == MixBlend.setup) pose.inherit = bone.data.setup.inherit;
return;
}
var frames:Array<Float> = frames;
if (time < frames[0]) {
if (blend == MixBlend.setup || blend == MixBlend.first) bone.inherit = bone.data.inherit;
return;
}
bone.inherit = Inherit.values[Std.int(frames[Timeline.search(frames, time, ENTRIES) + INHERIT])];
if (blend == MixBlend.setup || blend == MixBlend.first) pose.inherit = bone.data.setup.inherit;
} else
pose.inherit = Inherit.values[Std.int(frames[Timeline.search(frames, time, ENTRIES) + INHERIT])];
}
}

View File

@ -31,7 +31,7 @@ package spine.animation;
/** Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with
* alpha < 1.
*
*
* @see spine.animation.Timeline.apply() */
class MixBlend {
public var ordinal:Int = 0;
@ -40,25 +40,29 @@ class MixBlend {
this.ordinal = ordinal;
}
/** Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the
* setup value is set. */
/** Transitions between the setup and timeline values (the current value is not used). Before the first frame, the setup
* value is used.
*
* `setup` is intended to transition to or from the setup pose, not for animations layered on top of others. */
public static var setup(default, never):MixBlend = new MixBlend(0);
/** Transitions from the current value to the timeline value. Before the first frame, transitions from the current value to
* the setup value. Timelines which perform instant transitions, such as spine.animation.DrawOrderTimeline or
* spine.animation.AttachmentTimeline, use the setup value before the first frame.
*
* first is intended for the first animations applied, not for animations layered on top of those. */
/** Transitions between the current and timeline values. Before the first frame, transitions between the current and setup
* values. Timelines which perform instant transitions, such as {@link DrawOrderTimeline} or {@link AttachmentTimeline}, use
* the setup value before the first frame.
*
* `first` is intended for the first animations applied, not for animations layered on top of others. */
public static var first(default, never):MixBlend = new MixBlend(1);
/** Transitions from the current value to the timeline value. No change is made before the first frame (the current value is
* kept until the first frame).
*
* replace is intended for animations layered on top of others, not for the first animations applied. */
/** Transitions between the current and timeline values. No change is made before the first frame.
*
* `replace` is intended for animations layered on top of others, not for the first animations applied. */
public static var replace(default, never):MixBlend = new MixBlend(2);
/** Transitions from the current value to the current value plus the timeline value. No change is made before the first
* frame (the current value is kept until the first frame).
*
* add is intended for animations layered on top of others, not for the first animations applied. Properties
* set by additive animations must be set manually or by another animation before applying the additive animations, else the
* property values will increase each time the additive animations are applied. */
/** Transitions between the current value and the current plus timeline values. No change is made before the first frame.
*
* `add` is intended for animations layered on top of others, not for the first animations applied.
*
* Properties set by additive animations must be set manually or by another animation before applying the additive
* animations, else the property values will increase each time the additive animations are applied. */
public static var add(default, never):MixBlend = new MixBlend(3);
}

View File

@ -31,7 +31,7 @@ package spine.animation;
/** Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or
* mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied.
*
*
* @see spine.animation.Timeline.apply()
*/
class MixDirection {

View File

@ -35,7 +35,7 @@ import spine.Skeleton;
/** Changes a path constraint's PathConstraint.mixRotate, PathConstraint.mixX, and
* PathConstraint.mixY. */
class PathConstraintMixTimeline extends CurveTimeline {
class PathConstraintMixTimeline extends CurveTimeline implements ConstraintTimeline {
private static inline var ENTRIES:Int = 4;
private static inline var ROTATE:Int = 1;
private static inline var X:Int = 2;
@ -45,15 +45,19 @@ class PathConstraintMixTimeline extends CurveTimeline {
* applied. */
public var constraintIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, pathConstraintIndex:Int) {
super(frameCount, bezierCount, [Property.pathConstraintMix + "|" + pathConstraintIndex]);
this.constraintIndex = pathConstraintIndex;
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, Property.pathConstraintMix + "|" + constraintIndex);
this.constraintIndex = constraintIndex;
}
public override function getFrameEntries():Int {
return ENTRIES;
}
public function getConstraintIndex () {
return constraintIndex;
}
/** Sets the time and color for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -65,24 +69,24 @@ class PathConstraintMixTimeline extends CurveTimeline {
frames[frame + Y] = mixY;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var constraint:PathConstraint = skeleton.pathConstraints[constraintIndex];
if (!constraint.active)
return;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], PathConstraint);
if (!constraint.active) return;
var pose = appliedPose ? constraint.applied : constraint.pose;
var data:PathConstraintData;
if (time < frames[0]) {
data = constraint.data;
var setup = constraint.data.setup;
switch (blend) {
case MixBlend.setup:
constraint.mixRotate = data.mixRotate;
constraint.mixX = data.mixX;
constraint.mixY = data.mixY;
pose.mixRotate = setup.mixRotate;
pose.mixX = setup.mixX;
pose.mixY = setup.mixY;
case MixBlend.first:
constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha;
constraint.mixX += (data.mixX - constraint.mixX) * alpha;
constraint.mixY += (data.mixY - constraint.mixY) * alpha;
pose.mixRotate += (setup.mixRotate - pose.mixRotate) * alpha;
pose.mixX += (setup.mixX - pose.mixX) * alpha;
pose.mixY += (setup.mixY - pose.mixY) * alpha;
}
return;
}
@ -110,15 +114,20 @@ class PathConstraintMixTimeline extends CurveTimeline {
y = getBezierValue(time, i, Y, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
}
if (blend == MixBlend.setup) {
data = constraint.data;
constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha;
constraint.mixX = data.mixX + (x - data.mixX) * alpha;
constraint.mixY = data.mixY + (y - data.mixY) * alpha;
} else {
constraint.mixRotate += (rotate - constraint.mixRotate) * alpha;
constraint.mixX += (x - constraint.mixX) * alpha;
constraint.mixY += (y - constraint.mixY) * alpha;
switch (blend) {
case MixBlend.setup:
var setup = constraint.data.setup;
pose.mixRotate = setup.mixRotate + (rotate - setup.mixRotate) * alpha;
pose.mixX = setup.mixX + (x - setup.mixX) * alpha;
pose.mixY = setup.mixY + (y - setup.mixY) * alpha;
case MixBlend.first, MixBlend.replace:
pose.mixRotate += (rotate - pose.mixRotate) * alpha;
pose.mixX += (x - pose.mixX) * alpha;
pose.mixY += (y - pose.mixY) * alpha;
case MixBlend.add:
pose.mixRotate += rotate * alpha;
pose.mixX += x * alpha;
pose.mixY += y * alpha;
}
}
}

View File

@ -29,25 +29,20 @@
package spine.animation;
import spine.Event;
import spine.PathConstraint;
import spine.Skeleton;
/** Changes a path constraint's spine.PathConstraint.position. */
class PathConstraintPositionTimeline extends CurveTimeline1 {
/** The index of the path constraint in spine.Skeleton.pathConstraints that will be changed when this timeline is
* applied. */
public var constraintIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, pathConstraintIndex:Int) {
super(frameCount, bezierCount, [Property.pathConstraintPosition + "|" + pathConstraintIndex]);
this.constraintIndex = pathConstraintIndex;
class PathConstraintPositionTimeline extends ConstraintTimeline1 {
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.pathConstraintPosition);
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var constraint:PathConstraint = skeleton.pathConstraints[constraintIndex];
if (constraint.active)
constraint.position = getAbsoluteValue(time, alpha, blend, constraint.position, constraint.data.position);
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], PathConstraint);
if (constraint.active) {
var pose = appliedPose ? constraint.applied : constraint.pose;
pose.position = getAbsoluteValue(time, alpha, blend, pose.position, constraint.data.setup.position);
}
}
}

View File

@ -34,19 +34,18 @@ import spine.PathConstraint;
import spine.Skeleton;
/** Changes a path constraint's PathConstraint#spacing. */
class PathConstraintSpacingTimeline extends CurveTimeline1 {
/** The index of the path constraint in Skeleton#pathConstraints that will be changed when this timeline is
* applied. */
public var constraintIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, pathConstraintIndex:Int) {
super(frameCount, bezierCount, [Property.pathConstraintSpacing + "|" + pathConstraintIndex]);
this.constraintIndex = pathConstraintIndex;
class PathConstraintSpacingTimeline extends ConstraintTimeline1 {
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.pathConstraintSpacing);
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var constraint:PathConstraint = skeleton.pathConstraints[constraintIndex];
if (constraint.active) constraint.spacing = getAbsoluteValue(time, alpha, blend, constraint.spacing, constraint.data.spacing);
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], PathConstraint);
if (constraint.active) {
var pose = appliedPose ? constraint.applied : constraint.pose;
pose.spacing = getAbsoluteValue(time, alpha, blend, pose.spacing, constraint.data.setup.spacing);
}
}
}

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.damping. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.damping. */
class PhysicsConstraintDampingTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintDamping);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintDamping);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.damping;
public function get (pose: PhysicsConstraintPose):Float {
return pose.damping;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.damping;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.damping = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.damping = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.gravity. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.gravity. */
class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintGravity);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.gravity;
public function get (pose: PhysicsConstraintPose):Float {
return pose.gravity;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.gravity;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.gravity = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.gravity = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.inertia. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.inertia. */
class PhysicsConstraintInertiaTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintInertia);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintInertia);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.inertia;
public function get (pose: PhysicsConstraintPose):Float {
return pose.inertia;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.inertia;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.inertia = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.inertia = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.massInverse. The timeline values are not inverted. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.massInverse. The timeline values are not inverted. */
class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintMass);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintMass);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.massInverse;
public function get (pose: PhysicsConstraintPose):Float {
return pose.massInverse;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.massInverse;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.massInverse = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.massInverse = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.mix. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.mix. */
class PhysicsConstraintMixTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintMix);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintMix);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.mix;
public function get (pose: PhysicsConstraintPose):Float {
return pose.mix;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.mix;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.mix = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.mix = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -34,16 +34,17 @@ import spine.Event;
import spine.Skeleton;
/** Resets a physics constraint when specific animation times are reached. */
class PhysicsConstraintResetTimeline extends Timeline {
/** The index of the physics constraint in Skeleton#physicsConstraints that will be reset when this timeline is
* applied, or -1 if all physics constraints in the skeleton will be reset. */
public var constraintIndex:Int = 0;
class PhysicsConstraintResetTimeline extends Timeline implements ConstraintTimeline {
public var constraintIndex:Int;
/** @param physicsConstraintIndex -1 for all physics constraints in the skeleton. */
public function new(frameCount:Int, physicsConstraintIndex:Int) {
propertyIds = [Std.string(Property.physicsConstraintReset)];
super(frameCount, propertyIds);
constraintIndex = physicsConstraintIndex;
/** @param constraintIndex -1 for all physics constraints in the skeleton. */
public function new(frameCount:Int, constraintIndex:Int) {
super(frameCount, Property.physicsConstraintReset);
this.constraintIndex = constraintIndex;
}
public function getConstraintIndex () {
return constraintIndex;
}
public override function getFrameCount():Int {
@ -57,31 +58,30 @@ class PhysicsConstraintResetTimeline extends Timeline {
}
/** Resets the physics constraint when frames > lastTime and <= time. */
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, firedEvents:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint:PhysicsConstraint = null;
if (this.constraintIndex != -1) {
constraint = skeleton.physicsConstraints[constraintIndex];
if (constraintIndex != -1) {
constraint = cast(skeleton.constraints[constraintIndex], PhysicsConstraint);
if (!constraint.active) return;
}
var frames:Array<Float> = this.frames;
if (lastTime > time) // Apply events after lastTime for looped animations.
{
apply(skeleton, lastTime, 2147483647, [], alpha, blend, direction);
if (lastTime > time) { // Apply events after lastTime for looped animations.
apply(skeleton, lastTime, 2147483647, [], alpha, blend, direction, appliedPose);
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();
constraint.reset(skeleton);
else {
for (constraint in skeleton.physicsConstraints) {
if (constraint.active) constraint.reset();
for (constraint in skeleton.physics) {
if (constraint.active) constraint.reset(skeleton);
}
}
}

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.strength. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.strength. */
class PhysicsConstraintStrengthTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintStrength);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintStrength);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.strength;
public function get (pose: PhysicsConstraintPose):Float {
return pose.strength;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.strength;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.strength = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.strength = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -34,39 +34,37 @@ import spine.PathConstraint;
import spine.Skeleton;
/** The base class for most spine.PhysicsConstraint timelines. */
abstract class PhysicsConstraintTimeline extends CurveTimeline1 {
/** The index of the physics constraint in Skeleton.physicsConstraints that will be changed when this timeline
* is applied, or -1 if all physics constraints in the skeleton will be changed. */
public var constraintIndex:Int = 0;
abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
/**
* @param physicsConstraintIndex -1 for all physics constraints in the skeleton.
* @param constraintIndex -1 for all physics constraints in the skeleton.
*/
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int, property:Int) {
super(frameCount, bezierCount, [property + "|" + physicsConstraintIndex]);
constraintIndex = physicsConstraintIndex;
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int, property:Property) {
super(frameCount, bezierCount, constraintIndex, property);
}
public override function apply (skeleton:Skeleton, lastTime:Float, time:Float, firedEvents:Array<Event>, alpha:Float, blend:MixBlend, direction:MixDirection):Void {
var constraint:PhysicsConstraint;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
if (constraintIndex == -1) {
var value:Float = time >= frames[0] ? getCurveValue(time) : 0;
for (constraint in skeleton.physicsConstraints) {
if (constraint.active && global(constraint.data))
set(constraint, getAbsoluteValue2(time, alpha, blend, get(constraint), setup(constraint), value));
for (constraint in skeleton.physics) {
if (constraint.active && global(constraint.data)) {
var pose = appliedPose ? constraint.applied : constraint.pose;
set(pose, getAbsoluteValue2(time, alpha, blend, get(pose), get(constraint.data.setup), value));
}
}
} else {
constraint = skeleton.physicsConstraints[constraintIndex];
if (constraint.active) set(constraint, getAbsoluteValue(time, alpha, blend, get(constraint), setup(constraint)));
var constraint = cast(skeleton.constraints[constraintIndex], PhysicsConstraint);
if (constraint.active) {
var pose = appliedPose ? constraint.applied : constraint.pose;
set(pose, getAbsoluteValue(time, alpha, blend, get(pose), get(constraint.data.setup)));
}
}
}
abstract public function setup (constraint: PhysicsConstraint):Float;
abstract public function get (pose: PhysicsConstraintPose):Float;
abstract public function get (constraint: PhysicsConstraint):Float;
abstract public function set (constraint: PhysicsConstraint, value:Float):Void;
abstract public function set (pose: PhysicsConstraintPose, value:Float):Void;
abstract public function global (constraint: PhysicsConstraintData):Bool;
}

View File

@ -29,22 +29,18 @@
package spine.animation;
/** Changes a physics constraint's spine.PhysicsConstraint.wind. */
/** Changes a physics constraint's spine.PhysicsConstraintPose.wind. */
class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, physicsConstraintIndex:Int) {
super(frameCount, bezierCount, physicsConstraintIndex, Property.physicsConstraintWind);
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind);
}
public function setup (constraint: PhysicsConstraint):Float {
return constraint.data.wind;
public function get (pose: PhysicsConstraintPose):Float {
return pose.wind;
}
public function get (constraint: PhysicsConstraint):Float {
return constraint.wind;
}
public function set (constraint: PhysicsConstraint, value:Float):Void {
constraint.wind = value;
public function set (pose: PhysicsConstraintPose, value:Float):Void {
pose.wind = value;
}
public function global (constraint: PhysicsConstraintData):Bool {

View File

@ -32,43 +32,44 @@ package spine.animation;
/**
* Constants for animation property types.
*/
class Property {
public static inline var rotate:Int = 0;
public static inline var x:Int = 1;
public static inline var y:Int = 2;
public static inline var scaleX:Int = 3;
public static inline var scaleY:Int = 4;
public static inline var shearX:Int = 5;
public static inline var shearY:Int = 6;
public static inline var inherit:Int = 7;
enum abstract Property(String) from String to String {
var rotate = "0";
var x = "1";
var y = "2";
var scaleX = "3";
var scaleY = "4";
var shearX = "5";
var shearY = "6";
var inherit = "7";
public static inline var rgb:Int = 8;
public static inline var alpha:Int = 9;
public static inline var rgb2:Int = 10;
var rgb = "8";
var alpha = "9";
var rgb2 = "10";
public static inline var attachment:Int = 11;
public static inline var deform:Int = 12;
var attachment = "11";
var deform = "12";
public static inline var event:Int = 13;
public static inline var drawOrder:Int = 14;
var event = "13";
var drawOrder = "14";
public static inline var ikConstraint:Int = 15;
public static inline var transformConstraint:Int = 16;
var ikConstraint = "15";
var transformConstraint = "16";
public static inline var pathConstraintPosition:Int = 17;
public static inline var pathConstraintSpacing:Int = 18;
public static inline var pathConstraintMix:Int = 19;
var pathConstraintPosition = "17";
var pathConstraintSpacing = "18";
var pathConstraintMix = "19";
public static inline var physicsConstraintInertia:Int = 20;
public static inline var physicsConstraintStrength:Int = 21;
public static inline var physicsConstraintDamping:Int = 22;
public static inline var physicsConstraintMass:Int = 23;
public static inline var physicsConstraintWind:Int = 24;
public static inline var physicsConstraintGravity:Int = 25;
public static inline var physicsConstraintMix:Int = 26;
public static inline var physicsConstraintReset:Int = 27;
var physicsConstraintInertia = "20";
var physicsConstraintStrength = "21";
var physicsConstraintDamping = "22";
var physicsConstraintMass = "23";
var physicsConstraintWind = "24";
var physicsConstraintGravity = "25";
var physicsConstraintMix = "26";
var physicsConstraintReset = "27";
public static inline var sequence:Int = 28;
var sequence = "28";
public function new() {}
}
var sliderTime = "29";
var sliderMix = "30";
}

View File

@ -30,7 +30,7 @@
package spine.animation;
/** Changes the RGB for a slot's spine.Slot.color and spine.Slot.darkColor for two color tinting. */
class RGB2Timeline extends CurveTimeline implements SlotTimeline {
class RGB2Timeline extends SlotCurveTimeline {
private static inline var ENTRIES:Int = 7;
private static inline var R:Int = 1;
private static inline var G:Int = 2;
@ -39,23 +39,16 @@ class RGB2Timeline extends CurveTimeline implements SlotTimeline {
private static inline var G2:Int = 5;
private static inline var B2:Int = 6;
private var slotIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, slotIndex:Int) {
super(frameCount, bezierCount, [Property.rgb + "|" + slotIndex, Property.rgb2 + "|" + slotIndex]);
this.slotIndex = slotIndex;
super(frameCount, bezierCount, slotIndex,
Property.rgb + "|" + slotIndex,
Property.rgb2 + "|" + slotIndex);
}
public override function getFrameEntries():Int {
return ENTRIES;
}
/** The index of the slot in spine.Skeleton.slots that will be changed when this timeline is applied. The
* spine.Slot.darkColor must not be null. */
public function getSlotIndex():Int {
return slotIndex;
}
/** Sets the time, light color, and dark color for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -70,17 +63,11 @@ class RGB2Timeline extends CurveTimeline implements SlotTimeline {
frames[frame + B2] = b2;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
var light:Color = slot.color, dark:Color = slot.darkColor;
var setupLight:Color, setupDark:Color;
public function apply1 (slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var light:Color = pose.color, dark:Color = pose.darkColor;
if (time < frames[0]) {
setupLight = slot.data.color;
setupDark = slot.data.darkColor;
var setup = slot.data.setup;
var setupLight = setup.color, setupDark = setup.darkColor;
switch (blend) {
case MixBlend.setup:
light.r = setupLight.r;
@ -144,8 +131,8 @@ class RGB2Timeline extends CurveTimeline implements SlotTimeline {
dark.b = b2;
} else {
if (blend == MixBlend.setup) {
setupLight = slot.data.color;
setupDark = slot.data.darkColor;
var setup = slot.data.setup;
var setupLight = setup.color, setupDark = setup.darkColor;
light.r = setupLight.r;
light.g = setupLight.g;
light.b = setupLight.b;

View File

@ -30,7 +30,7 @@
package spine.animation;
/** Changes a slot's spine.Slot.getColor() and spine.Slot.getDarkColor() for two color tinting. */
class RGBA2Timeline extends CurveTimeline implements SlotTimeline {
class RGBA2Timeline extends SlotCurveTimeline {
private static inline var ENTRIES:Int = 8;
private static inline var R:Int = 1;
private static inline var G:Int = 2;
@ -40,27 +40,18 @@ class RGBA2Timeline extends CurveTimeline implements SlotTimeline {
private static inline var G2:Int = 6;
private static inline var B2:Int = 7;
private var slotIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, slotIndex:Int) {
super(frameCount, bezierCount, [
super(frameCount, bezierCount, slotIndex,
Property.rgb + "|" + slotIndex,
Property.alpha + "|" + slotIndex,
Property.rgb2 + "|" + slotIndex
]);
this.slotIndex = slotIndex;
);
}
public override function getFrameEntries():Int {
return ENTRIES;
}
/** The index of the slot in spine.Skeleton.getSlots() that will be changed when this timeline is applied. The
* spine.Slot.getDarkColor() must not be null. */
public function getSlotIndex():Int {
return slotIndex;
}
/** Sets the time, light color, and dark color for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -76,16 +67,11 @@ class RGBA2Timeline extends CurveTimeline implements SlotTimeline {
frames[frame + B2] = b2;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
var light:Color = slot.color, dark:Color = slot.darkColor;
public function apply1 (slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var light = pose.color, dark = pose.darkColor;
if (time < frames[0]) {
var setupLight:Color = slot.data.color,
setupDark:Color = slot.data.darkColor;
var setup = slot.data.setup;
var setupLight = setup.color, setupDark = setup.darkColor;
switch (blend) {
case MixBlend.setup:
light.setFromColor(setupLight);
@ -148,8 +134,9 @@ class RGBA2Timeline extends CurveTimeline implements SlotTimeline {
dark.b = b2;
} else {
if (blend == MixBlend.setup) {
light.setFromColor(slot.data.color);
dark.setFromColor(slot.data.darkColor);
var setup = slot.data.setup;
light.setFromColor(setup.color);
dark.setFromColor(setup.darkColor);
}
light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha);
dark.r += (r2 - dark.r) * alpha;

View File

@ -30,28 +30,23 @@
package spine.animation;
/** Changes a slot's spine.Slot.color. */
class RGBATimeline extends CurveTimeline implements SlotTimeline {
class RGBATimeline extends SlotCurveTimeline {
private static inline var ENTRIES:Int = 5;
private static inline var R:Int = 1;
private static inline var G:Int = 2;
private static inline var B:Int = 3;
private static inline var A:Int = 4;
private var slotIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, slotIndex:Int) {
super(frameCount, bezierCount, [Property.rgb + "|" + slotIndex, Property.alpha + "|" + slotIndex]);
this.slotIndex = slotIndex;
super(frameCount, bezierCount, slotIndex,
Property.rgb + "|" + slotIndex,
Property.alpha + "|" + slotIndex);
}
public override function getFrameEntries():Int {
return ENTRIES;
}
public function getSlotIndex():Int {
return slotIndex;
}
/** Sets the time and color for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -64,15 +59,10 @@ class RGBATimeline extends CurveTimeline implements SlotTimeline {
frames[frame + A] = a;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
var color:Color = slot.color;
public function apply1 (slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var color = pose.color;
if (time < frames[0]) {
var setup:Color = slot.data.color;
var setup:Color = slot.data.setup.color;
switch (blend) {
case MixBlend.setup:
color.setFromColor(setup);
@ -113,7 +103,7 @@ class RGBATimeline extends CurveTimeline implements SlotTimeline {
color.set(r, g, b, a);
} else {
if (blend == MixBlend.setup)
color.setFromColor(slot.data.color);
color.setFromColor(slot.data.setup.color);
color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
}
}

View File

@ -30,27 +30,20 @@
package spine.animation;
/** Changes the RGB for a slot's spine.Slot.color. */
class RGBTimeline extends CurveTimeline implements SlotTimeline {
class RGBTimeline extends SlotCurveTimeline {
private static inline var ENTRIES:Int = 4;
private static inline var R:Int = 1;
private static inline var G:Int = 2;
private static inline var B:Int = 3;
private var slotIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, slotIndex:Int) {
super(frameCount, bezierCount, [Property.rgb + "|" + slotIndex]);
this.slotIndex = slotIndex;
super(frameCount, bezierCount, slotIndex, Property.rgb + "|" + slotIndex);
}
public override function getFrameEntries():Int {
return ENTRIES;
}
public function getSlotIndex():Int {
return slotIndex;
}
/** Sets the time and color for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -62,15 +55,10 @@ class RGBTimeline extends CurveTimeline implements SlotTimeline {
frames[frame + B] = b;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var slot:Slot = skeleton.slots[slotIndex];
if (!slot.bone.active)
return;
var color:Color = slot.color, setup:Color;
public function apply1 (slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var color = pose.color;
if (time < frames[0]) {
setup = slot.data.color;
var setup = slot.data.setup.color;
switch (blend) {
case MixBlend.setup:
color.r = setup.r;
@ -112,7 +100,7 @@ class RGBTimeline extends CurveTimeline implements SlotTimeline {
color.b = b;
} else {
if (blend == MixBlend.setup) {
setup = slot.data.color;
var setup = slot.data.setup.color;
color.r = setup.r;
color.g = setup.g;
color.b = setup.b;

View File

@ -29,27 +29,14 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.Skeleton;
/** Changes a bone's local rotation. */
class RotateTimeline extends CurveTimeline1 implements BoneTimeline {
public var boneIndex:Int = 0;
class RotateTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.rotate + "|" + boneIndex]);
super(frameCount, bezierCount, boneIndex, Property.rotate);
this.boneIndex = boneIndex;
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active)
bone.rotation = getRelativeValue(time, alpha, blend, bone.rotation, bone.data.rotation);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection):Void {
pose.rotation = getRelativeValue(time, alpha, blend, pose.rotation, setup.rotation);
}
}

View File

@ -35,97 +35,86 @@ import spine.MathUtils;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.scaleX and spine.Bone.scaleY. */
class ScaleTimeline extends CurveTimeline2 implements BoneTimeline {
private var boneIndex:Int = 0;
class ScaleTimeline extends BoneTimeline2 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.scaleX + "|" + boneIndex, Property.scaleY + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.scaleX, Property.scaleY);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (!bone.active)
return;
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
bone.scaleX = bone.data.scaleX;
bone.scaleY = bone.data.scaleY;
pose.scaleX = setup.scaleX;
pose.scaleY = setup.scaleY;
case MixBlend.first:
bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha;
bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha;
pose.scaleX += (setup.scaleX - pose.scaleX) * alpha;
pose.scaleY += (setup.scaleY - pose.scaleY) * alpha;
}
return;
}
var x:Float = 0, y:Float = 0;
var i:Int = Timeline.search(frames, time, CurveTimeline2.ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / CurveTimeline2.ENTRIES)]);
var i:Int = Timeline.search(frames, time, BoneTimeline2.ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / BoneTimeline2.ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
x = frames[i + CurveTimeline2.VALUE1];
y = frames[i + CurveTimeline2.VALUE2];
var t:Float = (time - before) / (frames[i + CurveTimeline2.ENTRIES] - before);
x += (frames[i + CurveTimeline2.ENTRIES + CurveTimeline2.VALUE1] - x) * t;
y += (frames[i + CurveTimeline2.ENTRIES + CurveTimeline2.VALUE2] - y) * t;
x = frames[i + BoneTimeline2.VALUE1];
y = frames[i + BoneTimeline2.VALUE2];
var t:Float = (time - before) / (frames[i + BoneTimeline2.ENTRIES] - before);
x += (frames[i + BoneTimeline2.ENTRIES + BoneTimeline2.VALUE1] - x) * t;
y += (frames[i + BoneTimeline2.ENTRIES + BoneTimeline2.VALUE2] - y) * t;
case CurveTimeline.STEPPED:
x = frames[i + CurveTimeline2.VALUE1];
y = frames[i + CurveTimeline2.VALUE2];
x = frames[i + BoneTimeline2.VALUE1];
y = frames[i + BoneTimeline2.VALUE2];
default:
x = getBezierValue(time, i, CurveTimeline2.VALUE1, curveType - CurveTimeline.BEZIER);
y = getBezierValue(time, i, CurveTimeline2.VALUE2, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
x = getBezierValue(time, i, BoneTimeline2.VALUE1, curveType - CurveTimeline.BEZIER);
y = getBezierValue(time, i, BoneTimeline2.VALUE2, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
}
x *= bone.data.scaleX;
y *= bone.data.scaleY;
x *= setup.scaleX;
y *= setup.scaleY;
if (alpha == 1) {
if (blend == MixBlend.add) {
bone.scaleX += x - bone.data.scaleX;
bone.scaleY += y - bone.data.scaleY;
pose.scaleX += x - setup.scaleX;
pose.scaleY += y - setup.scaleY;
} else {
bone.scaleX = x;
bone.scaleY = y;
pose.scaleX = x;
pose.scaleY = y;
}
} else {
var bx:Float = 0, by:Float = 0;
if (direction == MixDirection.mixOut) {
switch (blend) {
case MixBlend.setup:
bx = bone.data.scaleX;
by = bone.data.scaleY;
bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha;
bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha;
bx = setup.scaleX;
by = setup.scaleY;
pose.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha;
pose.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha;
case MixBlend.first, MixBlend.replace:
bx = bone.scaleX;
by = bone.scaleY;
bone.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha;
bone.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha;
bx = pose.scaleX;
by = pose.scaleY;
pose.scaleX = bx + (Math.abs(x) * MathUtils.signum(bx) - bx) * alpha;
pose.scaleY = by + (Math.abs(y) * MathUtils.signum(by) - by) * alpha;
case MixBlend.add:
bone.scaleX = (x - bone.data.scaleX) * alpha;
bone.scaleY = (y - bone.data.scaleY) * alpha;
pose.scaleX = (x - setup.scaleX) * alpha;
pose.scaleY = (y - setup.scaleY) * alpha;
}
} else {
switch (blend) {
case MixBlend.setup:
bx = Math.abs(bone.data.scaleX) * MathUtils.signum(x);
by = Math.abs(bone.data.scaleY) * MathUtils.signum(y);
bone.scaleX = bx + (x - bx) * alpha;
bone.scaleY = by + (y - by) * alpha;
bx = Math.abs(setup.scaleX) * MathUtils.signum(x);
by = Math.abs(setup.scaleY) * MathUtils.signum(y);
pose.scaleX = bx + (x - bx) * alpha;
pose.scaleY = by + (y - by) * alpha;
case MixBlend.first, MixBlend.replace:
bx = Math.abs(bone.scaleX) * MathUtils.signum(x);
by = Math.abs(bone.scaleY) * MathUtils.signum(y);
bone.scaleX = bx + (x - bx) * alpha;
bone.scaleY = by + (y - by) * alpha;
bx = Math.abs(pose.scaleX) * MathUtils.signum(x);
by = Math.abs(pose.scaleY) * MathUtils.signum(y);
pose.scaleX = bx + (x - bx) * alpha;
pose.scaleY = by + (y - by) * alpha;
case MixBlend.add:
bone.scaleX += (x - bone.data.scaleX) * alpha;
bone.scaleY += (y - bone.data.scaleY) * alpha;
pose.scaleX += (x - setup.scaleX) * alpha;
pose.scaleY += (y - setup.scaleY) * alpha;
}
}
}

View File

@ -29,27 +29,13 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.MathUtils;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.scaleX. */
class ScaleXTimeline extends CurveTimeline1 implements BoneTimeline {
private var boneIndex:Int = 0;
class ScaleXTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.scaleX + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.scaleX);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active) bone.scaleX = getScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.scaleX);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
pose.scaleX = getScaleValue(time, alpha, blend, direction, pose.scaleX, setup.scaleX);
}
}

View File

@ -29,28 +29,13 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.MathUtils;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.scaleY. */
class ScaleYTimeline extends CurveTimeline1 implements BoneTimeline {
private var boneIndex:Int = 0;
class ScaleYTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.scaleY + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.scaleY);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active) bone.scaleY = getScaleValue(time, alpha, blend, direction, bone.scaleY, bone.data.scaleY);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
pose.scaleY = getScaleValue(time, alpha, blend, direction, pose.scaleY, setup.scaleY);
}
}

View File

@ -42,9 +42,8 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
var attachment:HasTextureRegion;
public function new(frameCount:Int, slotIndex:Int, attachment:HasTextureRegion) {
super(frameCount, [
Std.string(Property.sequence) + "|" + Std.string(slotIndex) + "|" + Std.string(attachment.sequence.id)
]);
super(frameCount,
Std.string(Property.sequence) + "|" + Std.string(slotIndex) + "|" + Std.string(attachment.sequence.id));
this.slotIndex = slotIndex;
this.attachment = attachment;
}
@ -71,12 +70,14 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
frames[frame + SequenceTimeline.DELAY] = delay;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var slot = skeleton.slots[this.slotIndex];
if (!slot.bone.active)
return;
var slotAttachment = slot.attachment;
if (!slot.bone.active) return;
var pose = appliedPose ? slot.applied : slot.pose;
var slotAttachment = pose.attachment;
var attachment = cast(this.attachment, Attachment);
if (slotAttachment != attachment) {
if (!Std.isOfType(slotAttachment, VertexAttachment) || cast(slotAttachment, VertexAttachment).timelineAttachment != attachment)
@ -84,13 +85,12 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
}
if (direction == MixDirection.mixOut) {
if (blend == MixBlend.setup) slot.sequenceIndex = -1;
if (blend == MixBlend.setup) pose.sequenceIndex = -1;
return;
}
if (time < frames[0]) {
if (blend == MixBlend.setup || blend == MixBlend.first)
slot.sequenceIndex = -1;
if (blend == MixBlend.setup || blend == MixBlend.first) pose.sequenceIndex = -1;
return;
}
@ -127,6 +127,6 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
index = n - index;
}
}
slot.sequenceIndex = index;
pose.sequenceIndex = index;
}
}

View File

@ -29,70 +29,54 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.shearX and spine.Bone.shearY. */
class ShearTimeline extends CurveTimeline2 implements BoneTimeline {
private var boneIndex:Int = 0;
class ShearTimeline extends BoneTimeline2 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.shearX + "|" + boneIndex, Property.shearY + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.shearX, Property.shearY);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (!bone.active)
return;
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
bone.shearX = bone.data.shearX;
bone.shearY = bone.data.shearY;
pose.shearX = setup.shearX;
pose.shearY = setup.shearY;
case MixBlend.first:
bone.shearX += (bone.data.shearX - bone.shearX) * alpha;
bone.shearY += (bone.data.shearY - bone.shearY) * alpha;
pose.shearX += (setup.shearX - pose.shearX) * alpha;
pose.shearY += (setup.shearY - pose.shearY) * alpha;
}
return;
}
var x:Float = 0, y:Float = 0;
var i:Int = Timeline.search(frames, time, CurveTimeline2.ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / CurveTimeline2.ENTRIES)]);
var i:Int = Timeline.search(frames, time, BoneTimeline2.ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / BoneTimeline2.ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
x = frames[i + CurveTimeline2.VALUE1];
y = frames[i + CurveTimeline2.VALUE2];
var t:Float = (time - before) / (frames[i + CurveTimeline2.ENTRIES] - before);
x += (frames[i + CurveTimeline2.ENTRIES + CurveTimeline2.VALUE1] - x) * t;
y += (frames[i + CurveTimeline2.ENTRIES + CurveTimeline2.VALUE2] - y) * t;
x = frames[i + BoneTimeline2.VALUE1];
y = frames[i + BoneTimeline2.VALUE2];
var t:Float = (time - before) / (frames[i + BoneTimeline2.ENTRIES] - before);
x += (frames[i + BoneTimeline2.ENTRIES + BoneTimeline2.VALUE1] - x) * t;
y += (frames[i + BoneTimeline2.ENTRIES + BoneTimeline2.VALUE2] - y) * t;
case CurveTimeline.STEPPED:
x = frames[i + CurveTimeline2.VALUE1];
y = frames[i + CurveTimeline2.VALUE2];
x = frames[i + BoneTimeline2.VALUE1];
y = frames[i + BoneTimeline2.VALUE2];
default:
x = getBezierValue(time, i, CurveTimeline2.VALUE1, curveType - CurveTimeline.BEZIER);
y = getBezierValue(time, i, CurveTimeline2.VALUE2, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
x = getBezierValue(time, i, BoneTimeline2.VALUE1, curveType - CurveTimeline.BEZIER);
y = getBezierValue(time, i, BoneTimeline2.VALUE2, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
}
switch (blend) {
case MixBlend.setup:
bone.shearX = bone.data.shearX + x * alpha;
bone.shearY = bone.data.shearY + y * alpha;
pose.shearX = setup.shearX + x * alpha;
pose.shearY = setup.shearY + y * alpha;
case MixBlend.first, MixBlend.replace:
bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha;
bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha;
pose.shearX += (setup.shearX + x - pose.shearX) * alpha;
pose.shearY += (setup.shearY + y - pose.shearY) * alpha;
case MixBlend.add:
bone.shearX += x * alpha;
bone.shearY += y * alpha;
pose.shearX += x * alpha;
pose.shearY += y * alpha;
}
}
}

View File

@ -29,26 +29,13 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.shearX. */
class ShearXTimeline extends CurveTimeline1 implements BoneTimeline {
private var boneIndex:Int = 0;
class ShearXTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.shearX + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.shearX);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active) bone.shearX = getRelativeValue(time, alpha, blend, bone.shearX, bone.data.shearX);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
pose.shearX = getRelativeValue(time, alpha, blend, pose.shearX, setup.shearX);
}
}

View File

@ -29,26 +29,13 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.Skeleton;
/** Changes a bone's local Bone.shearY. */
class ShearYTimeline extends CurveTimeline1 implements BoneTimeline {
private var boneIndex:Int = 0;
class ShearYTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.shearY + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.shearY);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active) bone.shearY = getRelativeValue(time, alpha, blend, bone.shearY, bone.data.shearY);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
pose.shearY = getRelativeValue(time, alpha, blend, pose.shearY, setup.shearY);
}
}

View File

@ -0,0 +1,49 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.animation;
import haxe.macro.Type.VarAccess;
/** Changes a slider's spine.SliderPose.mix. */
class SliderMixTimeline extends ConstraintTimeline1 {
public function new (frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.sliderMix);
}
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], Slider);
if (constraint.active) {
var pose = appliedPose ? constraint.applied : constraint.pose;
pose.mix = getAbsoluteValue(time, alpha, blend, pose.mix, constraint.data.setup.mix);
}
}
}

View File

@ -0,0 +1,47 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.animation;
/** Changes a slider's spine.SliderPose.time. */
class SliderTimeline extends ConstraintTimeline1 {
public function new (frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.sliderTime);
}
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], Slider);
if (constraint.active) {
var pose = appliedPose ? constraint.applied : constraint.pose;
pose.time = getAbsoluteValue(time, alpha, blend, pose.time, constraint.data.setup.time);
}
}
}

View File

@ -0,0 +1,52 @@
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, 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.
*****************************************************************************/
package spine.animation;
abstract class SlotCurveTimeline extends CurveTimeline implements SlotTimeline {
public final slotIndex:Int;
public function new (frameCount:Int, bezierCount:Int, slotIndex:Int, propertyIds:...String) {
super(frameCount, bezierCount, ...propertyIds);
this.slotIndex = slotIndex;
}
public function getSlotIndex () {
return slotIndex;
}
public function apply (skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection, appliedPose:Bool) {
var slot = skeleton.slots[slotIndex];
if (slot.bone.active) apply1(slot, appliedPose ? slot.applied : slot.pose, time, alpha, blend);
}
abstract function apply1 (slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend):Void;
}

View File

@ -33,7 +33,7 @@ import spine.Event;
import spine.Skeleton;
/** The base class for all timelines. */
class Timeline {
abstract class Timeline {
/** Uniquely encodes both the type of this timeline and the skeleton properties that it affects. */
public var propertyIds:Array<String>;
/** The time in seconds and any other values for each frame. */
@ -42,7 +42,7 @@ class Timeline {
/**
* @param propertyIds Unique identifiers for the properties the timeline modifies.
*/
public function new(frameCount:Int, propertyIds:Array<String>) {
public function new(frameCount:Int, propertyIds:...String) {
this.propertyIds = propertyIds;
frames = new Array<Float>();
frames.resize(frameCount * getFrameEntries());
@ -82,10 +82,9 @@ class Timeline {
* @param blend Controls how mixing is applied when alpha < 1.
* @param direction Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions,
* such as spine.animation.DrawOrderTimeline or spine.animation.AttachmentTimeline, and others such as spine.animation.ScaleTimeline.
*/
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend, direction:MixDirection):Void {
throw new SpineException("Timeline implementations must override apply()");
}
* @param appliedPose True to to modify the applied pose. */
abstract public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool):Void;
/** Linear search using a stride of 1.
* @param time Must be >= the first value in frames.

View File

@ -29,15 +29,10 @@
package spine.animation;
import spine.Event;
import spine.Skeleton;
import spine.TransformConstraint;
import spine.TransformConstraintData;
/** Changes a transform constraint's spine.TransformConstraint.mixRotate, spine.TransformConstraint.mixX,
* spine.TransformConstraint.mixY, spine.TransformConstraint.mixScaleX,
* spine.TransformConstraint.mixScaleY, and spine.TransformConstraint.mixShearY. */
class TransformConstraintTimeline extends CurveTimeline {
class TransformConstraintTimeline extends CurveTimeline implements ConstraintTimeline {
static public inline var ENTRIES:Int = 7;
private static inline var ROTATE:Int = 1;
private static inline var X:Int = 2;
@ -50,15 +45,19 @@ class TransformConstraintTimeline extends CurveTimeline {
* timeline is applied. */
public var constraintIndex:Int = 0;
public function new(frameCount:Int, bezierCount:Int, transformConstraintIndex:Int) {
super(frameCount, bezierCount, [Property.transformConstraint + "|" + transformConstraintIndex]);
this.constraintIndex = transformConstraintIndex;
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, Property.transformConstraint + "|" + constraintIndex);
this.constraintIndex = constraintIndex;
}
public override function getFrameEntries():Int {
return ENTRIES;
}
public function getConstraintIndex () {
return constraintIndex;
}
/** Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds. */
@ -73,30 +72,30 @@ class TransformConstraintTimeline extends CurveTimeline {
frames[frame + SHEARY] = mixShearY;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var constraint:TransformConstraint = skeleton.transformConstraints[constraintIndex];
if (!constraint.active)
return;
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float,
blend:MixBlend, direction:MixDirection, appliedPose:Bool) {
var constraint = cast(skeleton.constraints[constraintIndex], TransformConstraint);
if (!constraint.active) return;
var pose = appliedPose ? constraint.applied : constraint.pose;
var data:TransformConstraintData;
if (time < frames[0]) {
data = constraint.data;
var setup = constraint.data.setup;
switch (blend) {
case MixBlend.setup:
constraint.mixRotate = data.mixRotate;
constraint.mixX = data.mixX;
constraint.mixY = data.mixY;
constraint.mixScaleX = data.mixScaleX;
constraint.mixScaleY = data.mixScaleY;
constraint.mixShearY = data.mixShearY;
pose.mixRotate = setup.mixRotate;
pose.mixX = setup.mixX;
pose.mixY = setup.mixY;
pose.mixScaleX = setup.mixScaleX;
pose.mixScaleY = setup.mixScaleY;
pose.mixShearY = setup.mixShearY;
case MixBlend.first:
constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha;
constraint.mixX += (data.mixX - constraint.mixX) * alpha;
constraint.mixY += (data.mixY - constraint.mixY) * alpha;
constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha;
constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha;
constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha;
pose.mixRotate += (setup.mixRotate - pose.mixRotate) * alpha;
pose.mixX += (setup.mixX - pose.mixX) * alpha;
pose.mixY += (setup.mixY - pose.mixY) * alpha;
pose.mixScaleX += (setup.mixScaleX - pose.mixScaleX) * alpha;
pose.mixScaleY += (setup.mixScaleY - pose.mixScaleY) * alpha;
pose.mixShearY += (setup.mixShearY - pose.mixShearY) * alpha;
}
return;
}
@ -136,21 +135,29 @@ class TransformConstraintTimeline extends CurveTimeline {
shearY = getBezierValue(time, i, SHEARY, curveType + CurveTimeline.BEZIER_SIZE * 5 - CurveTimeline.BEZIER);
}
if (blend == MixBlend.setup) {
data = constraint.data;
constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha;
constraint.mixX = data.mixX + (x - data.mixX) * alpha;
constraint.mixY = data.mixY + (y - data.mixY) * alpha;
constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha;
constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha;
constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha;
} else {
constraint.mixRotate += (rotate - constraint.mixRotate) * alpha;
constraint.mixX += (x - constraint.mixX) * alpha;
constraint.mixY += (y - constraint.mixY) * alpha;
constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha;
constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha;
constraint.mixShearY += (shearY - constraint.mixShearY) * alpha;
switch (blend) {
case MixBlend.setup:
var setup = constraint.data.setup;
pose.mixRotate = setup.mixRotate + (rotate - setup.mixRotate) * alpha;
pose.mixX = setup.mixX + (x - setup.mixX) * alpha;
pose.mixY = setup.mixY + (y - setup.mixY) * alpha;
pose.mixScaleX = setup.mixScaleX + (scaleX - setup.mixScaleX) * alpha;
pose.mixScaleY = setup.mixScaleY + (scaleY - setup.mixScaleY) * alpha;
pose.mixShearY = setup.mixShearY + (shearY - setup.mixShearY) * alpha;
case MixBlend.first, MixBlend.replace:
pose.mixRotate += (rotate - pose.mixRotate) * alpha;
pose.mixX += (x - pose.mixX) * alpha;
pose.mixY += (y - pose.mixY) * alpha;
pose.mixScaleX += (scaleX - pose.mixScaleX) * alpha;
pose.mixScaleY += (scaleY - pose.mixScaleY) * alpha;
pose.mixShearY += (shearY - pose.mixShearY) * alpha;
case MixBlend.add:
pose.mixRotate += rotate * alpha;
pose.mixX += x * alpha;
pose.mixY += y * alpha;
pose.mixScaleX += scaleX * alpha;
pose.mixScaleY += scaleY * alpha;
pose.mixShearY += shearY * alpha;
}
}
}

View File

@ -34,66 +34,54 @@ import spine.Event;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.x and spine.Bone.y. */
class TranslateTimeline extends CurveTimeline2 implements BoneTimeline {
public var boneIndex:Int = 0;
class TranslateTimeline extends BoneTimeline2 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.x + "|" + boneIndex, Property.y + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.x, Property.y);
}
public function getBoneIndex():Int {
return boneIndex;
}
override public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (!bone.active)
return;
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection):Void {
if (time < frames[0]) {
switch (blend) {
case MixBlend.setup:
bone.x = bone.data.x;
bone.y = bone.data.y;
pose.x = setup.x;
pose.y = setup.y;
case MixBlend.first:
bone.x += (bone.data.x - bone.x) * alpha;
bone.y += (bone.data.y - bone.y) * alpha;
pose.x += (setup.x - pose.x) * alpha;
pose.y += (setup.y - pose.y) * alpha;
}
return;
}
var x:Float = 0, y:Float = 0;
var i:Int = Timeline.search(frames, time, CurveTimeline2.ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / CurveTimeline2.ENTRIES)]);
var i:Int = Timeline.search(frames, time, BoneTimeline2.ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / BoneTimeline2.ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
x = frames[i + CurveTimeline2.VALUE1];
y = frames[i + CurveTimeline2.VALUE2];
var t:Float = (time - before) / (frames[i + CurveTimeline2.ENTRIES] - before);
x += (frames[i + CurveTimeline2.ENTRIES + CurveTimeline2.VALUE1] - x) * t;
y += (frames[i + CurveTimeline2.ENTRIES + CurveTimeline2.VALUE2] - y) * t;
x = frames[i + BoneTimeline2.VALUE1];
y = frames[i + BoneTimeline2.VALUE2];
var t:Float = (time - before) / (frames[i + BoneTimeline2.ENTRIES] - before);
x += (frames[i + BoneTimeline2.ENTRIES + BoneTimeline2.VALUE1] - x) * t;
y += (frames[i + BoneTimeline2.ENTRIES + BoneTimeline2.VALUE2] - y) * t;
case CurveTimeline.STEPPED:
x = frames[i + CurveTimeline2.VALUE1];
y = frames[i + CurveTimeline2.VALUE2];
x = frames[i + BoneTimeline2.VALUE1];
y = frames[i + BoneTimeline2.VALUE2];
default:
x = getBezierValue(time, i, CurveTimeline2.VALUE1, curveType - CurveTimeline.BEZIER);
y = getBezierValue(time, i, CurveTimeline2.VALUE2, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
x = getBezierValue(time, i, BoneTimeline2.VALUE1, curveType - CurveTimeline.BEZIER);
y = getBezierValue(time, i, BoneTimeline2.VALUE2, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
}
switch (blend) {
case MixBlend.setup:
bone.x = bone.data.x + x * alpha;
bone.y = bone.data.y + y * alpha;
pose.x = setup.x + x * alpha;
pose.y = setup.y + y * alpha;
case MixBlend.first, MixBlend.replace:
bone.x += (bone.data.x + x - bone.x) * alpha;
bone.y += (bone.data.y + y - bone.y) * alpha;
pose.x += (setup.x + x - pose.x) * alpha;
pose.y += (setup.y + y - pose.y) * alpha;
case MixBlend.add:
bone.x += x * alpha;
bone.y += y * alpha;
pose.x += x * alpha;
pose.y += y * alpha;
}
}
}

View File

@ -34,21 +34,13 @@ import spine.Event;
import spine.Skeleton;
/** Changes a bone's local spine.Bone.x. */
class TranslateXTimeline extends CurveTimeline1 implements BoneTimeline {
public var boneIndex:Int = 0;
class TranslateXTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.x + "|" + boneIndex]);
super(frameCount, bezierCount, boneIndex, Property.x);
this.boneIndex = boneIndex;
}
public function getBoneIndex():Int {
return boneIndex;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active) bone.x = getRelativeValue(time, alpha, blend, bone.x, bone.data.x);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
pose.x = getRelativeValue(time, alpha, blend, pose.x, setup.x);
}
}

View File

@ -29,26 +29,13 @@
package spine.animation;
import spine.Bone;
import spine.Event;
import spine.Skeleton;
/** Changes a bone's local y translation. */
class TranslateYTimeline extends CurveTimeline1 implements BoneTimeline {
public var boneIndex:Int = 0;
class TranslateYTimeline extends BoneTimeline1 {
public function new(frameCount:Int, bezierCount:Int, boneIndex:Int) {
super(frameCount, bezierCount, [Property.y + "|" + boneIndex]);
this.boneIndex = boneIndex;
super(frameCount, bezierCount, boneIndex, Property.y);
}
public function getBoneIndex():Int {
return boneIndex;
}
public override function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend,
direction:MixDirection):Void {
var bone:Bone = skeleton.bones[boneIndex];
if (bone.active) bone.y = getRelativeValue(time, alpha, blend, bone.y, bone.data.y);
public function apply1 (pose:BoneLocal, setup:BoneLocal, time:Float, alpha:Float, blend:MixBlend, direction:MixDirection) {
pose.y = getRelativeValue(time, alpha, blend, pose.y, setup.y);
}
}

View File

@ -125,11 +125,12 @@ class MeshAttachment extends VertexAttachment implements HasTextureRegion {
i += 2;
}
return;
default:
u -= region.offsetX / textureWidth;
v -= (region.originalHeight - region.offsetY - region.height) / textureHeight;
width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight;
}
u -= region.offsetX / textureWidth;
v -= (region.originalHeight - region.offsetY - region.height) / textureHeight;
width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight;
} else if (region == null) {
u = v = 0;
width = height = 1;
@ -194,10 +195,10 @@ class MeshAttachment extends VertexAttachment implements HasTextureRegion {
}
/** If the attachment has a sequence, the region may be changed. */
public override function computeWorldVertices(slot:Slot, start:Int, count:Int, worldVertices:Array<Float>, offset:Int, stride:Int):Void {
public override function computeWorldVertices(skeleton:Skeleton, slot:Slot, start:Int, count:Int, worldVertices:Array<Float>, offset:Int, stride:Int):Void {
if (sequence != null)
sequence.apply(slot, this);
super.computeWorldVertices(slot, start, count, worldVertices, offset, stride);
sequence.apply(slot.applied, this);
super.computeWorldVertices(skeleton, slot, start, count, worldVertices, offset, stride);
}
/** Returns a new mesh with the parentMesh set to this mesh's parent mesh, if any, else to this mesh. */

View File

@ -29,7 +29,7 @@
package spine.attachments;
import spine.Bone;
import spine.BonePose;
import spine.Color;
import spine.MathUtils;
@ -51,13 +51,13 @@ class PointAttachment extends VertexAttachment {
super(name);
}
public function computeWorldPosition(bone:Bone, point:Array<Float>):Array<Float> {
public function computeWorldPosition(bone:BonePose, point:Array<Float>):Array<Float> {
point[0] = x * bone.a + y * bone.b + bone.worldX;
point[1] = x * bone.c + y * bone.d + bone.worldY;
return point;
}
public function computeWorldRotation(bone:Bone):Float {
public function computeWorldRotation(bone:BonePose):Float {
var r:Float = this.rotation * MathUtils.degRad, cos:Float = Math.cos(r), sin:Float = Math.sin(r);
var x:Float = cos * bone.a + sin * bone.b;
var y:Float = cos * bone.c + sin * bone.d;

View File

@ -66,13 +66,13 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
public var sequence:Sequence;
/** For each of the 4 vertices, a pair of x,y values that is the local position of the vertex.
*
*
* See RegionAttachment.updateRegion(). */
private var offsets:Array<Float> = new Array<Float>();
public var uvs:Array<Float> = new Array<Float>();
/**
/**
* @param name The attachment name.
* @param path The path used to find the region for the attachment.
*/
@ -148,17 +148,16 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
/** Transforms the attachment's four vertices to world coordinates. If the attachment has a RegionAttachment.sequence, the region may
* be changed.
*
*
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
* @param worldVertices The output world vertices. Must have a length >= offset + 8.
* @param offset The worldVertices index to begin writing values.
* @param stride The number of worldVertices entries between the value pairs written. */
public function computeWorldVertices(slot:Slot, worldVertices:Array<Float>, offset:Int, stride:Int):Void {
if (sequence != null)
sequence.apply(slot, this);
if (sequence != null) sequence.apply(slot.applied, this);
var bone = slot.bone;
var vertexOffset = this.offsets;
var bone = slot.bone.applied;
var x = bone.worldX, y = bone.worldY;
var a = bone.a, b = bone.b, c = bone.c, d = bone.d;
var offsetX:Float = 0, offsetY:Float = 0;

View File

@ -34,7 +34,7 @@ import spine.Skeleton;
import spine.Slot;
/** Base class for an attachment with vertices that are transformed by one or more bones and can be deformed by a slot's
* spine.Slot.deform. */
* spine.SlotPose.deform. */
class VertexAttachment extends Attachment {
private static var nextID:Int = 0;
@ -42,15 +42,19 @@ class VertexAttachment extends Attachment {
* the vertex followed by that many bone indices, which is the index of the bone in spine.Skeleton.bones. Will be null
* if this attachment has no weights. */
public var bones:Array<Int>;
/** The vertex positions in the bone's coordinate system. For a non-weighted attachment, the values are `x,y`
* entries for each vertex. For a weighted attachment, the values are `x,y,weight` entries for each bone affecting
* each vertex. */
public var vertices = new Array<Float>();
/** The maximum number of world vertex values that can be output by
* computeWorldVertices() using the `count` parameter. */
public var worldVerticesLength:Int = 0;
/** Returns a unique ID for this attachment. */
public var id:Int = nextID++;
/** Timelines for the timeline attachment are also applied to this attachment.
* May be null if no attachment-specific timelines should be applied. */
public var timelineAttachment:VertexAttachment;
@ -60,7 +64,7 @@ class VertexAttachment extends Attachment {
timelineAttachment = this;
}
/** Transforms the attachment's local vertices to world coordinates. If the slot's spine.Slot.deform is
/** Transforms the attachment's local vertices to world coordinates. If the slot's spine.SlotPose.deform is
* not empty, it is used to deform the vertices.
*
* @see https://esotericsoftware.com/spine-runtime-skeletons#World-transforms World transforms in the Spine Runtimes Guide
@ -70,61 +74,42 @@ class VertexAttachment extends Attachment {
* `stride` / 2.
* @param offset The `worldVertices` index to begin writing values.
* @param stride The number of `worldVertices` entries between the value pairs written. */
public function computeWorldVertices(slot:Slot, start:Int, count:Int, worldVertices:Array<Float>, offset:Int, stride:Int):Void {
public function computeWorldVertices(skeleton:Skeleton, slot:Slot, start:Int, count:Int, worldVertices:Array<Float>, offset:Int, stride:Int):Void {
count = offset + (count >> 1) * stride;
var skeleton:Skeleton = slot.skeleton;
var deform:Array<Float> = slot.deform;
var v:Int, w:Int, n:Int, i:Int, skip:Int, b:Int, f:Int;
var vx:Float, vy:Float;
var wx:Float, wy:Float;
var bone:Bone;
var deform:Array<Float> = slot.applied.deform;
var vertices = vertices;
if (bones == null) {
if (deform.length > 0)
vertices = deform;
bone = slot.bone;
var x:Float = bone.worldX;
var y:Float = bone.worldY;
var a:Float = bone.a,
bb:Float = bone.b,
c:Float = bone.c,
d:Float = bone.d;
v = start;
w = offset;
if (deform.length > 0) vertices = deform;
var bone = slot.bone.applied;
var x = bone.worldX, y = bone.worldY;
var a = bone.a, b = bone.b, c = bone.c, d = bone.d;
var v = start, w = offset;
while (w < count) {
vx = vertices[v];
vy = vertices[v + 1];
worldVertices[w] = vx * a + vy * bb + x;
var vx = vertices[v], vy = vertices[v + 1];
worldVertices[w] = vx * a + vy * b + x;
worldVertices[w + 1] = vx * c + vy * d + y;
v += 2;
w += stride;
}
return;
}
v = 0;
skip = 0;
i = 0;
var v = 0, skip = 0, i = 0;
while (i < start) {
n = bones[v];
var n = bones[v];
v += n + 1;
skip += n;
i += 2;
}
var skeletonBones:Array<Bone> = skeleton.bones;
var skeletonBones = skeleton.bones;
if (deform.length == 0) {
w = offset;
b = skip * 3;
var w = offset, b = skip * 3;
while (w < count) {
wx = 0;
wy = 0;
n = bones[v++];
var wx = 0., wy = 0.;
var n = bones[v++];
n += v;
while (v < n) {
bone = skeletonBones[bones[v]];
vx = vertices[b];
vy = vertices[b + 1];
var weight:Float = vertices[b + 2];
var bone = skeletonBones[bones[v]].applied;
var vx = vertices[b], vy = vertices[b + 1], weight = vertices[b + 2];
wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight;
wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight;
v++;
@ -135,19 +120,14 @@ class VertexAttachment extends Attachment {
w += stride;
}
} else {
w = offset;
b = skip * 3;
f = skip << 1;
var w = offset, b = skip * 3, f = skip << 1;
while (w < count) {
wx = 0;
wy = 0;
n = bones[v++];
var wx = 0., wy = 0.;
var n = bones[v++];
n += v;
while (v < n) {
bone = skeletonBones[bones[v]];
vx = vertices[b] + deform[f];
vy = vertices[b + 1] + deform[f + 1];
var weight = vertices[b + 2];
var bone = skeletonBones[bones[v]].applied;
var vx = vertices[b] + deform[f], vy = vertices[b + 1] + deform[f + 1], weight = vertices[b + 2];
wx += (vx * bone.a + vy * bone.b + bone.worldX) * weight;
wy += (vx * bone.c + vy * bone.d + bone.worldY) * weight;
v++;

View File

@ -201,7 +201,7 @@ class SkeletonSprite extends FlxObject
for (slot in drawOrder) {
var clippedVertexSize:Int = clipper.isClipping() ? 2 : vertexSize;
if (!slot.bone.active) {
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
continue;
}
@ -239,7 +239,7 @@ class SkeletonSprite extends FlxObject
clipper.clipStart(slot, clip);
continue;
} else {
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
continue;
}
@ -312,7 +312,7 @@ class SkeletonSprite extends FlxObject
mesh.draw();
}
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
}
clipper.clipEnd();
}

View File

@ -111,7 +111,7 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
for (slot in drawOrder) {
if (!slot.bone.active) {
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
continue;
}
@ -178,13 +178,13 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
clipper.clipStart(slot, clip);
continue;
} else {
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
continue;
}
a = slot.color.a * attachmentColor.a;
if (a == 0) {
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
continue;
}
rgb = Color.rgb(Std.int(r * slot.color.r * attachmentColor.r), Std.int(g * slot.color.g * attachmentColor.g),
@ -230,7 +230,7 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
painter.batchMesh(mesh);
}
clipper.clipEndWithSlot(slot);
clipper.clipEnd(slot);
}
painter.state.blendMode = originalBlendMode;
clipper.clipEnd();