2025-07-29 18:05:18 +02:00

447 lines
14 KiB
Haxe

/******************************************************************************
* 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.starling;
import spine.animation.Animation;
import starling.animation.IAnimatable;
import openfl.geom.Matrix;
import openfl.geom.Point;
import openfl.geom.Rectangle as OpenFlRectangle;
import spine.Bone;
import spine.Rectangle;
import spine.Skeleton;
import spine.SkeletonClipping;
import spine.SkeletonData;
import spine.Slot;
import spine.animation.AnimationState;
import spine.animation.AnimationStateData;
import spine.animation.MixBlend;
import spine.animation.MixDirection;
import spine.attachments.ClippingAttachment;
import spine.attachments.MeshAttachment;
import spine.attachments.RegionAttachment;
import starling.display.BlendMode;
import starling.display.DisplayObject;
import starling.rendering.IndexData;
import starling.rendering.Painter;
import starling.rendering.VertexData;
import starling.textures.Texture;
import starling.utils.Color;
import starling.utils.MatrixUtil;
import starling.utils.Max;
/** A starling display object that draws a skeleton. */
class SkeletonSprite extends DisplayObject implements IAnimatable {
static private var _tempPoint:Point = new Point();
static private var _tempMatrix:Matrix = new Matrix();
static private var _tempVertices:Array<Float> = new Array<Float>();
static private var blendModes:Array<String> = [BlendMode.NORMAL, BlendMode.ADD, BlendMode.MULTIPLY, BlendMode.SCREEN];
private var _skeleton:Skeleton;
public var _state:AnimationState;
private var _smoothing:String = "bilinear";
public static var clipper(default, never):SkeletonClipping = new SkeletonClipping();
private static var QUAD_INDICES:Array<Int> = [0, 1, 2, 2, 3, 0];
private var tempLight:spine.Color = new spine.Color(0, 0, 0);
private var tempDark:spine.Color = new spine.Color(0, 0, 0);
public var beforeUpdateWorldTransforms:SkeletonSprite->Void = function(_) {};
public var afterUpdateWorldTransforms:SkeletonSprite->Void = function(_) {};
/** Creates an uninitialized SkeletonSprite. The skeleton and animation state must be set before use. */
public function new(skeletonData:SkeletonData, animationStateData:AnimationStateData = null) {
super();
Bone.yDown = true;
_skeleton = new Skeleton(skeletonData);
_skeleton.updateWorldTransform(Physics.update);
_state = new AnimationState(animationStateData != null ? animationStateData : new AnimationStateData(skeletonData));
}
override public function render(painter:Painter):Void {
var clipper:SkeletonClipping = SkeletonSprite.clipper;
painter.state.alpha *= skeleton.color.a;
var originalBlendMode:String = painter.state.blendMode;
var r:Float = skeleton.color.r * 255;
var g:Float = skeleton.color.g * 255;
var b:Float = skeleton.color.b * 255;
var drawOrder:Array<Slot> = skeleton.drawOrder;
var attachmentColor:spine.Color;
var rgb:Int;
var a:Float;
var dark:Int;
var mesh:SkeletonMesh = null;
var verticesLength:Int;
var verticesCount:Int;
var indicesLength:Int;
var indexData:IndexData;
var indices:Array<Int> = null;
var vertexData:VertexData;
var uvs:Array<Float>;
for (slot in drawOrder) {
if (!slot.bone.active) {
clipper.clipEnd(slot);
continue;
}
var worldVertices:Array<Float> = _tempVertices;
var pose = slot.applied;
var attachment = pose.attachment;
if (Std.isOfType(attachment, RegionAttachment)) {
var region:RegionAttachment = cast(attachment, RegionAttachment);
verticesLength = 8;
verticesCount = verticesLength >> 1;
if (worldVertices.length < verticesLength)
worldVertices.resize(verticesLength);
region.computeWorldVertices(slot, worldVertices, 0, 2);
mesh = null;
if (Std.isOfType(region.rendererObject, SkeletonMesh)) {
mesh = cast(region.rendererObject, SkeletonMesh);
mesh.texture = region.region.texture;
indices = QUAD_INDICES;
} else {
mesh = region.rendererObject = new SkeletonMesh(cast(region.region.texture, Texture));
indexData = mesh.getIndexData();
indices = QUAD_INDICES;
for (i in 0...indices.length) {
indexData.setIndex(i, indices[i]);
}
indexData.numIndices = indices.length;
indexData.trim();
}
indexData = mesh.getIndexData();
attachmentColor = region.color;
uvs = region.uvs;
} else if (Std.isOfType(attachment, MeshAttachment)) {
var meshAttachment:MeshAttachment = cast(attachment, MeshAttachment);
verticesLength = meshAttachment.worldVerticesLength;
verticesCount = verticesLength >> 1;
if (worldVertices.length < verticesLength)
worldVertices.resize(verticesLength);
meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, worldVertices, 0, 2);
mesh = null;
if (Std.isOfType(meshAttachment.rendererObject, SkeletonMesh)) {
mesh = cast(meshAttachment.rendererObject, SkeletonMesh);
mesh.texture = meshAttachment.region.texture;
indices = meshAttachment.triangles;
} else {
mesh = meshAttachment.rendererObject = new SkeletonMesh(cast(meshAttachment.region.texture, Texture));
indexData = mesh.getIndexData();
indices = meshAttachment.triangles;
indicesLength = indices.length;
for (i in 0...indicesLength) {
indexData.setIndex(i, indices[i]);
}
indexData.numIndices = indicesLength;
indexData.trim();
}
indexData = mesh.getIndexData();
attachmentColor = meshAttachment.color;
uvs = meshAttachment.uvs;
} else if (Std.isOfType(attachment, ClippingAttachment)) {
var clip:ClippingAttachment = cast(attachment, ClippingAttachment);
clipper.clipEnd(slot);
clipper.clipStart(skeleton, slot, clip);
continue;
} else {
clipper.clipEnd(slot);
continue;
}
a = pose.color.a * attachmentColor.a;
if (a == 0) {
clipper.clipEnd(slot);
continue;
}
rgb = Color.rgb(Std.int(r * pose.color.r * attachmentColor.r), Std.int(g * pose.color.g * attachmentColor.g),
Std.int(b * pose.color.b * attachmentColor.b));
if (pose.darkColor == null) {
dark = Color.rgb(0, 0, 0);
} else {
dark = Color.rgb(Std.int(pose.darkColor.r * 255), Std.int(pose.darkColor.g * 255), Std.int(pose.darkColor.b * 255));
}
if (clipper.isClipping() && clipper.clipTriangles(worldVertices, indices, indices.length, uvs)) {
// Need to create a new mesh here, see https://github.com/EsotericSoftware/spine-runtimes/issues/1125
mesh = new SkeletonMesh(mesh.texture);
indexData = mesh.getIndexData();
verticesCount = clipper.clippedVertices.length >> 1;
worldVertices = clipper.clippedVertices;
uvs = clipper.clippedUvs;
indices = clipper.clippedTriangles;
indicesLength = indices.length;
indexData.numIndices = indicesLength;
indexData.trim();
for (i in 0...indicesLength) {
indexData.setIndex(i, indices[i]);
}
}
vertexData = mesh.getVertexData();
vertexData.numVertices = verticesCount;
vertexData.colorize("color", rgb, a);
var ii:Int = 0;
for (i in 0...verticesCount) {
mesh.setVertexPosition(i, worldVertices[ii], worldVertices[ii + 1]);
mesh.setTexCoords(i, uvs[ii], uvs[ii + 1]);
ii += 2;
}
if (indexData.numIndices > 0 && vertexData.numVertices > 0) {
painter.state.blendMode = blendModes[slot.data.blendMode.ordinal];
painter.batchMesh(mesh);
}
clipper.clipEnd(slot);
}
painter.state.blendMode = originalBlendMode;
clipper.clipEnd();
}
override public function hitTest(localPoint:Point):DisplayObject {
if (!this.visible || !this.touchable)
return null;
var minX:Float = Max.MAX_VALUE;
var minY:Float = Max.MAX_VALUE;
var maxX:Float = -Max.MAX_VALUE;
var maxY:Float = -Max.MAX_VALUE;
var slots:Array<Slot> = skeleton.slots;
var worldVertices:Array<Float> = _tempVertices;
var empty:Bool = true;
for (i in 0...slots.length) {
var slot:Slot = slots[i];
var pose = slot.applied;
var attachment = pose.attachment;
if (attachment == null)
continue;
var verticesLength:Int;
if (Std.isOfType(attachment, RegionAttachment)) {
var region:RegionAttachment = cast(attachment, RegionAttachment);
verticesLength = 8;
region.computeWorldVertices(slot, worldVertices, 0, 2);
} else if (Std.isOfType(attachment, MeshAttachment)) {
var mesh:MeshAttachment = cast(attachment, MeshAttachment);
verticesLength = mesh.worldVerticesLength;
if (worldVertices.length < verticesLength)
worldVertices.resize(verticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, verticesLength, worldVertices, 0, 2);
} else {
continue;
}
if (verticesLength != 0) {
empty = false;
}
var ii:Int = 0;
while (ii < verticesLength) {
var x:Float = worldVertices[ii],
y:Float = worldVertices[ii + 1];
minX = minX < x ? minX : x;
minY = minY < y ? minY : y;
maxX = maxX > x ? maxX : x;
maxY = maxY > y ? maxY : y;
ii += 2;
}
}
if (empty) {
return null;
}
var temp:Float;
if (maxX < minX) {
temp = maxX;
maxX = minX;
minX = temp;
}
if (maxY < minY) {
temp = maxY;
maxY = minY;
minY = temp;
}
if (localPoint.x >= minX && localPoint.x < maxX && localPoint.y >= minY && localPoint.y < maxY) {
return this;
}
return null;
}
override public function getBounds(targetSpace:DisplayObject, resultRect:OpenFlRectangle = null):OpenFlRectangle {
if (resultRect == null) {
resultRect = new OpenFlRectangle();
}
if (targetSpace == this) {
resultRect.setTo(0, 0, 0, 0);
} else if (targetSpace == parent) {
resultRect.setTo(x, y, 0, 0);
} else {
getTransformationMatrix(targetSpace, _tempMatrix);
MatrixUtil.transformCoords(_tempMatrix, 0, 0, _tempPoint);
resultRect.setTo(_tempPoint.x, _tempPoint.y, 0, 0);
}
return resultRect;
}
public function getAnimationBounds(animation:Animation, clip:Bool = true):Rectangle {
var clipper = clip ? SkeletonSprite.clipper : null;
_skeleton.setupPose();
var steps = 100, time = 0.;
var stepTime = animation.duration != 0 ? animation.duration / steps : 0;
var minX = 100000000.,
maxX = -100000000.,
minY = 100000000.,
maxY = -100000000.;
for (i in 0...steps) {
animation.apply(_skeleton, time, time, false, [], 1, MixBlend.setup, MixDirection.mixIn, false);
_skeleton.updateWorldTransform(Physics.update);
var boundsSkel = _skeleton.getBounds(clipper);
if (!Math.isNaN(boundsSkel.x) && !Math.isNaN(boundsSkel.y) && !Math.isNaN(boundsSkel.width) && !Math.isNaN(boundsSkel.height)) {
minX = Math.min(boundsSkel.x, minX);
minY = Math.min(boundsSkel.y, minY);
maxX = Math.max(boundsSkel.x + boundsSkel.width, maxX);
maxY = Math.max(boundsSkel.y + boundsSkel.height, maxY);
} else
throw new SpineException("Animation bounds are invalid: " + animation.name);
time += stepTime;
}
var bounds = new Rectangle();
bounds.x = minX;
bounds.y = minY;
bounds.width = maxX - minX;
bounds.height = maxY - minY;
return bounds;
}
public var skeleton(get, never):Skeleton;
private function get_skeleton():Skeleton {
return _skeleton;
}
public var state(get, never):AnimationState;
private function get_state():AnimationState {
return _state;
}
public var smoothing(get, set):String;
private function get_smoothing():String {
return _smoothing;
}
private function set_smoothing(smoothing:String):String {
_smoothing = smoothing;
return _smoothing;
}
public function advanceTime(time:Float):Void {
_state.update(time);
_state.apply(skeleton);
this.beforeUpdateWorldTransforms(this);
skeleton.update(time);
skeleton.updateWorldTransform(Physics.update);
this.afterUpdateWorldTransforms(this);
this.setRequiresRedraw();
}
public function skeletonToHaxeWorldCoordinates(point:Array<Float>):Void {
var transform = this.transformationMatrix;
var a = transform.a,
b = transform.b,
c = transform.c,
d = transform.d,
tx = transform.tx,
ty = transform.ty;
var x = point[0];
var y = point[1];
point[0] = x * a + y * c + tx;
point[1] = x * b + y * d + ty;
}
public function haxeWorldCoordinatesToSkeleton(point:Array<Float>):Void {
var transform = this.transformationMatrix.clone().invert();
var a = transform.a,
b = transform.b,
c = transform.c,
d = transform.d,
tx = transform.tx,
ty = transform.ty;
var x = point[0];
var y = point[1];
point[0] = x * a + y * c + tx;
point[1] = x * b + y * d + ty;
}
public function haxeWorldCoordinatesToBone(point:Array<Float>, bone:Bone):Void {
this.haxeWorldCoordinatesToSkeleton(point);
var parentBone = bone.parent;
if (parentBone != null) {
parentBone.applied.worldToLocal(point);
} else {
bone.applied.worldToLocal(point);
}
}
override public function dispose():Void {
if (_state != null) {
_state.clearListeners();
_state = null;
}
if (_skeleton != null)
_skeleton = null;
dispatchEventWith(starling.events.Event.REMOVE_FROM_JUGGLER);
removeFromParent();
// this will remove also all starling event listeners
super.dispose();
}
}