[haxe] Port latest libgdx timeline, sequence, draw order, and follow-up fixes. See #2989.

This commit is contained in:
Davide Tantillo 2026-03-16 18:10:00 +01:00
parent f7ed100aa8
commit 2ba91231f3
29 changed files with 858 additions and 643 deletions

View File

@ -29,20 +29,14 @@
package spine;
interface HasTextureRegion {
/** The name used to find the region. */
interface HasSequence {
public var path:String;
/** Sets the region used to draw the attachment. After setting the region or if the region's properties are changed,
* updateRegion() must be called. */
public var region:TextureRegion;
/** The color to tint the attachment. */
public var color:Color;
public var sequence:Sequence;
/** Updates any values the attachment calculates using the region. Must be called after setting the
* region or if the region's properties are changed. */
public function updateRegion():Void;
/** Calls Sequence.update() on this attachment's sequence. */
public function updateSequence():Void;
}

View File

@ -29,7 +29,11 @@
package spine;
/** A sequence for an attachment with multiple texture regions, which can be used for animation. */
import spine.attachments.MeshAttachment;
import spine.attachments.RegionAttachment;
/** Holds texture regions, UVs, and vertex offsets for rendering a region or mesh attachment. Regions must
* be populated and update() called before use. */
class Sequence {
private static var _nextID = 0;
@ -37,49 +41,109 @@ class Sequence {
public var id = _nextID++;
public var regions:Array<TextureRegion>;
public var pathSuffix:Bool;
public var uvs:Array<Array<Float>>;
public var offsets:Array<Array<Float>>;
public var start = 0;
public var digits = 0;
/** The index of the region to show for the setup pose. */
public var setupIndex = 0;
public function new(count:Int) {
public function new(count:Int, pathSuffix:Bool) {
this.regions = new Array<TextureRegion>();
this.regions.resize(count);
this.pathSuffix = pathSuffix;
}
/** Copy constructor. */
public function copy():Sequence {
var copy = new Sequence(this.regions.length);
for (i in 0...this.regions.length) {
var regionCount = this.regions.length;
var copy = new Sequence(regionCount, this.pathSuffix);
for (i in 0...regionCount)
copy.regions[i] = this.regions[i];
}
copy.start = this.start;
copy.digits = this.digits;
copy.setupIndex = this.setupIndex;
if (this.uvs != null) {
var length = this.uvs[0].length;
copy.uvs = new Array<Array<Float>>();
copy.uvs.resize(regionCount);
for (i in 0...regionCount) {
copy.uvs[i] = new Array<Float>();
copy.uvs[i].resize(length);
for (j in 0...length)
copy.uvs[i][j] = this.uvs[i][j];
}
}
if (this.offsets != null) {
copy.offsets = new Array<Array<Float>>();
copy.offsets.resize(regionCount);
for (i in 0...regionCount) {
copy.offsets[i] = new Array<Float>();
copy.offsets[i].resize(8);
for (j in 0...8)
copy.offsets[i][j] = this.offsets[i][j];
}
}
return copy;
}
public function apply(slot:SlotPose, attachment:HasTextureRegion) {
var index:Int = slot.sequenceIndex;
if (index == -1)
index = this.setupIndex;
if (index >= this.regions.length)
index = this.regions.length - 1;
var region = this.regions[index];
if (attachment.region != region) {
attachment.region = region;
attachment.updateRegion();
/** Computes UVs and offsets for the specified attachment. Must be called if the regions or attachment properties are
* changed. */
public function update(attachment:HasSequence):Void {
var regionCount = this.regions.length;
if (Std.isOfType(attachment, RegionAttachment)) {
var region:RegionAttachment = cast(attachment, RegionAttachment);
this.uvs = new Array<Array<Float>>();
this.uvs.resize(regionCount);
this.offsets = new Array<Array<Float>>();
this.offsets.resize(regionCount);
for (i in 0...regionCount) {
this.uvs[i] = new Array<Float>();
this.uvs[i].resize(8);
this.offsets[i] = new Array<Float>();
this.offsets[i].resize(8);
RegionAttachment.computeUVs(this.regions[i], region.x, region.y, region.scaleX, region.scaleY, region.rotation,
region.width, region.height, this.offsets[i], this.uvs[i]);
}
} else if (Std.isOfType(attachment, MeshAttachment)) {
var mesh:MeshAttachment = cast(attachment, MeshAttachment);
var regionUVs = mesh.regionUVs;
this.uvs = new Array<Array<Float>>();
this.uvs.resize(regionCount);
this.offsets = null;
for (i in 0...regionCount) {
this.uvs[i] = new Array<Float>();
this.uvs[i].resize(regionUVs.length);
MeshAttachment.computeUVs(this.regions[i], regionUVs, this.uvs[i]);
}
}
}
public function resolveIndex(pose:SlotPose):Int {
var index:Int = pose.sequenceIndex;
if (index == -1) index = this.setupIndex;
if (index >= this.regions.length) index = this.regions.length - 1;
return index;
}
public function getUVs(index:Int):Array<Float> {
return this.uvs[index];
}
public function getHasPathSuffix():Bool {
return this.pathSuffix;
}
public function getPath(basePath:String, index:Int):String {
if (!this.pathSuffix) return basePath;
var result = basePath;
var frame = Std.string(this.start + index);
for (i in 0...(this.digits - frame.length)) {
for (i in 0...(this.digits - frame.length))
result += "0";
}
result += frame;
return result;
}

View File

@ -443,7 +443,8 @@ class Skeleton {
verticesLength = 8;
_tempVertices.resize(verticesLength);
vertices = _tempVertices;
cast(attachment, RegionAttachment).computeWorldVertices(slot, vertices, 0, 2);
var region:RegionAttachment = cast(attachment, RegionAttachment);
region.computeWorldVertices(slot, region.getOffsets(slot.applied), vertices, 0, 2);
triangles = Skeleton.quadTriangles;
} else if (Std.isOfType(attachment, MeshAttachment)) {
var mesh:MeshAttachment = cast(attachment, MeshAttachment);

View File

@ -41,6 +41,7 @@ import spine.animation.BoneTimeline2;
import spine.animation.CurveTimeline1;
import spine.animation.CurveTimeline;
import spine.animation.DeformTimeline;
import spine.animation.DrawOrderFolderTimeline;
import spine.animation.DrawOrderTimeline;
import spine.animation.EventTimeline;
import spine.animation.IkConstraintTimeline;
@ -349,7 +350,7 @@ class SkeletonBinary {
data.slot = slots[input.readInt(true)];
var flags = input.readByte();
data.skinRequired = (flags & 1) != 0;
data.positionMode = PositionMode.values[(flags >> 1) & 2];
data.positionMode = PositionMode.values[(flags >> 1) & 1];
data.spacingMode = SpacingMode.values[(flags >> 2) & 3];
data.rotateMode = RotateMode.values[(flags >> 4) & 3];
if ((flags & 128) != 0)
@ -467,8 +468,7 @@ class SkeletonBinary {
throw new SpineException("Parent mesh not found: " + linkedMesh.parent);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? cast(parent, VertexAttachment) : linkedMesh.mesh;
linkedMesh.mesh.parentMesh = cast(parent, MeshAttachment);
if (linkedMesh.mesh.region != null)
linkedMesh.mesh.updateRegion();
linkedMesh.mesh.updateSequence();
}
linkedMeshes.resize(0);
@ -546,8 +546,10 @@ class SkeletonBinary {
return skin;
}
private function readSequence(input:BinaryInput):Sequence {
var sequence = new Sequence(input.readInt(true));
private function readSequence(input:BinaryInput, hasPathSuffix:Bool):Sequence {
if (!hasPathSuffix)
return new Sequence(1, false);
var sequence = new Sequence(input.readInt(true), true);
sequence.start = input.readInt(true);
sequence.digits = input.readInt(true);
sequence.setupIndex = input.readInt(true);
@ -574,7 +576,7 @@ class SkeletonBinary {
case AttachmentType.region:
path = (flags & 16) != 0 ? input.readStringRef() : null;
color = (flags & 32) != 0 ? input.readInt32() : 0xffffffff;
var sequence = (flags & 64) != 0 ? readSequence(input) : null;
var sequence = readSequence(input, (flags & 64) != 0);
rotation = (flags & 128) != 0 ? input.readFloat() : 0;
x = input.readFloat();
y = input.readFloat();
@ -597,9 +599,7 @@ class SkeletonBinary {
region.width = width * scale;
region.height = height * scale;
region.color.setFromRgba8888(color);
region.sequence = sequence;
if (region.region != null)
region.updateRegion();
region.updateSequence();
return region;
case AttachmentType.boundingbox:
vertices = readVertices(input, (flags & 16) != 0);
@ -618,7 +618,7 @@ class SkeletonBinary {
case AttachmentType.mesh:
path = (flags & 16) != 0 ? input.readStringRef() : name;
color = (flags & 32) != 0 ? input.readInt32() : 0xffffffff;
var sequence = (flags & 64) != 0 ? readSequence(input) : null;
var sequence = readSequence(input, (flags & 64) != 0);
var hullLength = input.readInt(true);
vertices = readVertices(input, (flags & 128) != 0);
var uvs:Array<Float> = readFloatArray(input, vertices.length, 1);
@ -637,28 +637,26 @@ class SkeletonBinary {
return null;
mesh.path = path;
mesh.color.setFromRgba8888(color);
mesh.hullLength = hullLength << 1;
if (vertices.bones.length > 0)
mesh.bones = vertices.bones;
mesh.vertices = vertices.vertices;
mesh.worldVerticesLength = vertices.length;
mesh.triangles = triangles;
mesh.regionUVs = uvs;
if (mesh.region != null)
mesh.updateRegion();
mesh.hullLength = hullLength << 1;
mesh.sequence = sequence;
mesh.triangles = triangles;
if (nonessential) {
mesh.edges = edges;
mesh.width = width * scale;
mesh.height = height * scale;
}
mesh.updateSequence();
return mesh;
case AttachmentType.linkedmesh:
path = (flags & 16) != 0 ? input.readStringRef() : name;
if (path == null)
throw new SpineException("Path of linked mesh must not be null");
color = (flags & 32) != 0 ? input.readInt32() : 0xffffffff;
var sequence = (flags & 64) != 0 ? this.readSequence(input) : null;
var sequence = readSequence(input, (flags & 64) != 0);
var inheritTimelines:Bool = (flags & 128) != 0;
var skinIndex = input.readInt(true);
var parent:String = input.readStringRef();
@ -672,7 +670,6 @@ class SkeletonBinary {
return null;
mesh.path = path;
mesh.color.setFromRgba8888(color);
mesh.sequence = sequence;
if (nonessential) {
mesh.width = width * scale;
mesh.height = height * scale;
@ -746,23 +743,54 @@ class SkeletonBinary {
vertices.vertices = readFloatArray(input, vertices.length, scale);
return vertices;
}
var n:Int = input.readInt(true);
var bones:Array<Int> = new Array<Int>();
var weights:Array<Float> = new Array<Float>();
var bonesArray:Array<Int> = new Array<Int>();
for (i in 0...vertexCount) {
var b:Int = 0, w:Int = 0;
while (b < n) {
var boneCount:Int = input.readInt(true);
bonesArray.push(boneCount);
bones[b++] = boneCount;
for (ii in 0...boneCount) {
bonesArray.push(input.readInt(true));
weights.push(input.readFloat() * scale);
weights.push(input.readFloat() * scale);
weights.push(input.readFloat());
bones[b++] = input.readInt(true);
weights[w] = input.readFloat() * scale;
weights[w + 1] = input.readFloat() * scale;
weights[w + 2] = input.readFloat();
w += 3;
}
}
vertices.vertices = weights;
vertices.bones = bonesArray;
vertices.bones = bones;
return vertices;
}
private function readDrawOrder(input:BinaryInput, slotCount:Int):Array<Int> {
var changeCount:Int = input.readInt(true);
if (changeCount == 0)
return null;
var drawOrder:Array<Int> = new Array<Int>();
drawOrder.resize(slotCount);
for (i in 0...slotCount)
drawOrder[i] = -1;
var unchanged:Array<Int> = new Array<Int>();
unchanged.resize(slotCount - changeCount);
var originalIndex:Int = 0, unchangedIndex:Int = 0;
for (i in 0...changeCount) {
var slotIndex:Int = input.readInt(true);
while (originalIndex != slotIndex)
unchanged[unchangedIndex++] = originalIndex++;
drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
}
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
var i:Int = slotCount - 1;
while (i >= 0) {
if (drawOrder[i] == -1)
drawOrder[i] = unchanged[--unchangedIndex];
i--;
}
return drawOrder;
}
private function readFloatArray(input:BinaryInput, n:Int, scale:Float):Array<Float> {
var array:Array<Float> = new Array<Float>();
if (scale == 1) {
@ -1337,7 +1365,7 @@ class SkeletonBinary {
}
timelines.push(deformTimeline);
case ATTACHMENT_SEQUENCE:
var timeline = new SequenceTimeline(frameCount, slotIndex, cast(attachment, HasTextureRegion));
var timeline = new SequenceTimeline(frameCount, slotIndex, cast(attachment, HasSequence));
for (frame in 0...frameCount) {
var time = input.readFloat();
var modeAndIndex = input.readInt32();
@ -1350,48 +1378,31 @@ class SkeletonBinary {
}
}
// Draw order timelines.
// Draw order timeline.
var slotCount:Int = skeletonData.slots.length;
var drawOrderCount:Int = input.readInt(true);
if (drawOrderCount > 0) {
var drawOrderTimeline:DrawOrderTimeline = new DrawOrderTimeline(drawOrderCount);
var slotCount:Int = skeletonData.slots.length;
for (i in 0...drawOrderCount) {
time = input.readFloat();
var offsetCount:Int = input.readInt(true);
var drawOrder:Array<Int> = new Array<Int>();
drawOrder.resize(slotCount);
var ii:Int = slotCount - 1;
while (ii >= 0) {
drawOrder[ii--] = -1;
}
var unchanged:Array<Int> = new Array<Int>();
unchanged.resize(slotCount - offsetCount);
var originalIndex:Int = 0, unchangedIndex:Int = 0;
for (ii in 0...offsetCount) {
slotIndex = input.readInt(true);
// Collect unchanged items.
while (originalIndex != slotIndex) {
unchanged[unchangedIndex++] = originalIndex++;
}
// Set changed items.
drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount) {
unchanged[unchangedIndex++] = originalIndex++;
}
// Fill in unchanged items.
ii = slotCount - 1;
while (ii >= 0) {
if (drawOrder[ii] == -1)
drawOrder[ii] = unchanged[--unchangedIndex];
ii--;
}
drawOrderTimeline.setFrame(i, time, drawOrder);
}
for (i in 0...drawOrderCount)
drawOrderTimeline.setFrame(i, input.readFloat(), readDrawOrder(input, slotCount));
timelines.push(drawOrderTimeline);
}
// Draw order folder timelines.
var folderCount:Int = input.readInt(true);
for (i in 0...folderCount) {
var folderSlotCount:Int = input.readInt(true);
var folderSlots:Array<Int> = new Array<Int>();
folderSlots.resize(folderSlotCount);
for (ii in 0...folderSlotCount)
folderSlots[ii] = input.readInt(true);
var keyCount:Int = input.readInt(true);
var folderTimeline = new DrawOrderFolderTimeline(keyCount, folderSlots, slotCount);
for (ii in 0...keyCount)
folderTimeline.setFrame(ii, input.readFloat(), readDrawOrder(input, folderSlotCount));
timelines.push(folderTimeline);
}
// Event timelines.
var eventCount:Int = input.readInt(true);
if (eventCount > 0) {

View File

@ -33,6 +33,7 @@ import haxe.io.Bytes;
import spine.animation.Animation;
import spine.atlas.TextureAtlas;
import spine.attachments.AtlasAttachmentLoader;
import spine.SliderData;
/** Stores the setup pose and all of the stateless data for a skeleton.
*
@ -191,6 +192,20 @@ class SkeletonData {
// --- Animations.
/** Collects animations used by slider constraints.
* Slider animations are designed to be applied by slider constraints rather than on their own. Applications that have a user
* choose an animation may want to exclude them. */
public function findSliderAnimations(animations:Array<Animation>):Array<Animation> {
for (constraint in constraints) {
if (Std.isOfType(constraint, SliderData)) {
var data:SliderData = cast(constraint, SliderData);
if (data.animation != null)
animations.push(data.animation);
}
}
return animations;
}
/** Finds an animation by comparing each animation's name. It is more efficient to cache the results of this method than to
* call it multiple times.
* @param animationName The name of the animation to find.

View File

@ -42,6 +42,7 @@ import spine.animation.AttachmentTimeline;
import spine.animation.CurveTimeline1;
import spine.animation.CurveTimeline;
import spine.animation.DeformTimeline;
import spine.animation.DrawOrderFolderTimeline;
import spine.animation.DrawOrderTimeline;
import spine.animation.EventTimeline;
import spine.animation.IkConstraintTimeline;
@ -497,8 +498,7 @@ class SkeletonJson {
throw new SpineException("Parent mesh not found: " + linkedMesh.parent);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? cast(parentMesh, VertexAttachment) : linkedMesh.mesh;
linkedMesh.mesh.parentMesh = cast(parentMesh, MeshAttachment);
if (linkedMesh.mesh.region != null)
linkedMesh.mesh.updateRegion();
linkedMesh.mesh.updateSequence();
}
linkedMeshes.resize(0);
@ -592,14 +592,11 @@ class SkeletonJson {
region.rotation = getFloat(map, "rotation");
region.width = getFloat(map, "width") * scale;
region.height = getFloat(map, "height") * scale;
region.sequence = sequence;
color = Reflect.getProperty(map, "color");
if (color != null) {
region.color.setFromString(color);
}
if (region.region != null)
region.updateRegion();
region.updateSequence();
return region;
case AttachmentType.mesh, AttachmentType.linkedmesh:
var path = getString(map, "path", name);
@ -616,7 +613,6 @@ class SkeletonJson {
mesh.width = getFloat(map, "width") * scale;
mesh.height = getFloat(map, "height") * scale;
mesh.sequence = sequence;
if (Reflect.field(map, "parent") != null) {
var inheritTimelines:Bool = Reflect.hasField(map, "timelines") ? cast(Reflect.field(map, "timelines"), Bool) : true;
@ -628,12 +624,11 @@ class SkeletonJson {
readVertices(map, mesh, uvs.length);
mesh.triangles = getIntArray(map, "triangles");
mesh.regionUVs = uvs;
if (mesh.region != null)
mesh.updateRegion();
if (Reflect.field(map, "edges") != null)
mesh.edges = getIntArray(map, "edges");
mesh.hullLength = getInt(map, "hull") * 2;
mesh.updateSequence();
return mesh;
case AttachmentType.boundingbox:
var box:BoundingBoxAttachment = attachmentLoader.newBoundingBoxAttachment(skin, name);
@ -689,14 +684,55 @@ class SkeletonJson {
private function readSequence(map:Dynamic) {
if (map == null)
return null;
var sequence = new Sequence(getInt(map, "count", 0));
return new Sequence(1, false);
var sequence = new Sequence(getInt(map, "count", 0), true);
sequence.start = getInt(map, "start", 1);
sequence.digits = getInt(map, "digits", 0);
sequence.setupIndex = getInt(map, "setup", 0);
return sequence;
}
/** @param folderSlots Slot names are resolved to positions within this array. If null, slot indices are used as positions. */
private function readDrawOrder(skeletonData:SkeletonData, keyMap:Dynamic, slotCount:Int, folderSlots:Array<Int>):Array<Int> {
var changes:Array<Dynamic> = Reflect.getProperty(keyMap, "offsets");
if (changes == null) return null;
var drawOrder:Array<Int> = new Array<Int>();
drawOrder.resize(slotCount);
for (i in 0...slotCount)
drawOrder[i] = -1;
var unchanged:Array<Int> = new Array<Int>();
unchanged.resize(slotCount - changes.length);
var originalIndex:Int = 0, unchangedIndex:Int = 0;
for (offsetMap in changes) {
var slot = skeletonData.findSlot(Reflect.getProperty(offsetMap, "slot"));
if (slot == null) throw new SpineException("Draw order slot not found: " + Reflect.getProperty(offsetMap, "slot"));
var index:Int;
if (folderSlots == null)
index = slot.index;
else {
index = -1;
for (i in 0...slotCount) {
if (folderSlots[i] == slot.index) {
index = i;
break;
}
}
if (index == -1) throw new SpineException("Slot not in folder: " + Reflect.getProperty(offsetMap, "slot"));
}
while (originalIndex != index)
unchanged[unchangedIndex++] = originalIndex++;
drawOrder[originalIndex + Reflect.getProperty(offsetMap, "offset")] = originalIndex++;
}
while (originalIndex < slotCount)
unchanged[unchangedIndex++] = originalIndex++;
var i:Int = slotCount - 1;
while (i >= 0) {
if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex];
i--;
}
return drawOrder;
}
private function readVertices(map:Dynamic, attachment:VertexAttachment, verticesLength:Int):Void {
attachment.worldVerticesLength = verticesLength;
var vertices:Array<Float> = getFloatArray(map, "vertices");
@ -1222,7 +1258,7 @@ class SkeletonJson {
slotMap = Reflect.field(attachmentsMap, slotMapName);
slotIndex = skeletonData.findSlot(slotMapName).index;
if (slotIndex == -1)
throw new SpineException("Slot not found: " + slotMapName);
throw new SpineException("Attachment slot not found: " + slotMapName);
for (attachmentMapName in Reflect.fields(slotMap)) {
var attachmentMap = Reflect.field(slotMap, attachmentMapName);
var attachment:Attachment = skin.getAttachment(slotIndex, attachmentMapName);
@ -1295,7 +1331,7 @@ class SkeletonJson {
timelines.push(deformTimeline);
case "sequence":
var timeline = new SequenceTimeline(timelineMap.length, slotIndex, cast(attachment, HasTextureRegion));
var timeline = new SequenceTimeline(timelineMap.length, slotIndex, cast(attachment, HasSequence));
var lastDelay:Float = 0;
var frame:Int = 0;
while (frame < timelineMap.length) {
@ -1316,54 +1352,44 @@ class SkeletonJson {
}
// Draw order timelines.
// Draw order timeline.
if (Reflect.hasField(map, "drawOrder")) {
var drawOrders:Array<Dynamic> = cast(Reflect.field(map, "drawOrder"), Array<Dynamic>);
if (drawOrders != null) {
var drawOrderTimeline:DrawOrderTimeline = new DrawOrderTimeline(drawOrders.length);
var slotCount:Int = skeletonData.slots.length;
frame = 0;
for (drawOrderMap in drawOrders) {
var drawOrder:Array<Int> = null;
var offsets:Array<Dynamic> = Reflect.getProperty(drawOrderMap, "offsets");
if (offsets != null) {
drawOrder = new Array<Int>();
drawOrder.resize(slotCount);
var i = slotCount - 1;
while (i >= 0) {
drawOrder[i--] = -1;
}
var unchanged:Array<Int> = new Array<Int>();
unchanged.resize(slotCount - offsets.length);
var originalIndex:Int = 0, unchangedIndex:Int = 0;
for (offsetMap in offsets) {
slotIndex = skeletonData.findSlot(Reflect.getProperty(offsetMap, "slot")).index;
if (slotIndex == -1)
throw new SpineException("Slot not found: " + Reflect.getProperty(offsetMap, "slot"));
// Collect unchanged items.
while (originalIndex != slotIndex) {
unchanged[unchangedIndex++] = originalIndex++;
}
// Set changed items.
drawOrder[originalIndex + Reflect.getProperty(offsetMap, "offset")] = originalIndex++;
}
// Collect remaining unchanged items.
while (originalIndex < slotCount) {
unchanged[unchangedIndex++] = originalIndex++;
}
// Fill in unchanged items.
i = slotCount - 1;
while (i >= 0) {
if (drawOrder[i] == -1)
drawOrder[i] = unchanged[--unchangedIndex];
i--;
}
}
drawOrderTimeline.setFrame(frame++, getFloat(drawOrderMap, "time"), drawOrder);
}
for (drawOrderMap in drawOrders)
drawOrderTimeline.setFrame(frame++, getFloat(drawOrderMap, "time"), readDrawOrder(skeletonData, drawOrderMap, slotCount, null));
timelines.push(drawOrderTimeline);
}
}
// Draw order folder timelines.
if (Reflect.hasField(map, "drawOrderFolder")) {
var drawOrderFolders:Array<Dynamic> = cast(Reflect.field(map, "drawOrderFolder"), Array<Dynamic>);
if (drawOrderFolders != null) {
for (timelineMap in drawOrderFolders) {
var slotEntries:Array<Dynamic> = cast(Reflect.field(timelineMap, "slots"), Array<Dynamic>);
var folderSlots:Array<Int> = new Array<Int>();
folderSlots.resize(slotEntries.length);
var ii:Int = 0;
for (slotEntry in slotEntries) {
var slot = skeletonData.findSlot(cast(slotEntry, String));
if (slot == null) throw new SpineException("Draw order folder slot not found: " + cast(slotEntry, String));
folderSlots[ii++] = slot.index;
}
var keys:Array<Dynamic> = cast(Reflect.field(timelineMap, "keys"), Array<Dynamic>);
var folderTimeline = new DrawOrderFolderTimeline(keys.length, folderSlots, skeletonData.slots.length);
frame = 0;
for (keyMap in keys)
folderTimeline.setFrame(frame++, getFloat(keyMap, "time"), readDrawOrder(skeletonData, keyMap, folderSlots.length, folderSlots));
timelines.push(folderTimeline);
}
}
}
// Event timelines.
if (Reflect.hasField(map, "events")) {
var eventsMap:Array<Dynamic> = cast(Reflect.field(map, "events"), Array<Dynamic>);

View File

@ -142,7 +142,7 @@ class Skin {
continue;
if (Std.isOfType(attachment.attachment, MeshAttachment)) {
var mesh = cast(attachment.attachment, MeshAttachment);
attachment.attachment = new MeshAttachment(mesh.name, mesh.path).newLinkedMesh();
attachment.attachment = mesh.newLinkedMesh();
setAttachment(attachment.slotIndex, attachment.name, attachment.attachment);
} else {
attachment.attachment = attachment.attachment.copy();

View File

@ -62,24 +62,28 @@ class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
return;
var color = (appliedPose ? slot.applied : slot.pose).color;
var a:Float = 0;
if (time < frames[0]) {
var setup:Color = slot.data.setup.color;
switch (blend) {
case MixBlend.setup:
color.a = setup.a;
return;
case MixBlend.first:
color.a += (setup.a - color.a) * alpha;
a = color.a + (setup.a - color.a) * alpha;
default:
return;
}
return;
}
var a:Float = getCurveValue(time);
if (alpha == 1) {
color.a = a;
} else {
if (blend == MixBlend.setup)
color.a = slot.data.setup.color.a;
color.a += (a - color.a) * alpha;
a = getCurveValue(time);
if (alpha != 1) {
if (blend == MixBlend.setup) {
var setup = slot.data.setup.color;
a = setup.a + (a - setup.a) * alpha;
} else
a = color.a + (a - color.a) * alpha;
}
}
color.a = a < 0 ? 0 : (a > 1 ? 1 : a);
}
}

View File

@ -266,7 +266,7 @@ class AnimationState {
attachments = true;
for (timeline in timelines) {
if (Std.isOfType(timeline, AttachmentTimeline)) {
applyAttachmentTimeline(cast(timeline, AttachmentTimeline), skeleton, applyTime, blend, attachments);
applyAttachmentTimeline(cast(timeline, AttachmentTimeline), skeleton, applyTime, blend, false, attachments);
} else {
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.mixIn, false);
}
@ -286,7 +286,7 @@ class AnimationState {
this.applyRotateTimeline(cast(timeline, RotateTimeline), skeleton, applyTime, alpha, timelineBlend, current.timelinesRotation,
ii << 1, firstFrame);
} else if (Std.isOfType(timeline, AttachmentTimeline)) {
this.applyAttachmentTimeline(cast(timeline, AttachmentTimeline), skeleton, applyTime, blend, attachments);
this.applyAttachmentTimeline(cast(timeline, AttachmentTimeline), skeleton, applyTime, blend, false, attachments);
} else {
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, timelineBlend, MixDirection.mixIn, false);
}
@ -366,7 +366,6 @@ class AnimationState {
from.totalAlpha = 0;
for (i in 0...timelineCount) {
var timeline:Timeline = timelines[i];
var direction:MixDirection = MixDirection.mixOut;
var timelineBlend:MixBlend;
var alpha:Float = 0;
switch (timelineMode[i]) {
@ -395,8 +394,9 @@ class AnimationState {
applyRotateTimeline(cast(timeline, RotateTimeline), skeleton, applyTime, alpha, timelineBlend, from.timelinesRotation, i << 1, firstFrame);
} else if (Std.isOfType(timeline, AttachmentTimeline)) {
applyAttachmentTimeline(cast(timeline, AttachmentTimeline), skeleton, applyTime,
timelineBlend, attachments && alpha >= from.alphaAttachmentThreshold);
timelineBlend, true, attachments && alpha >= from.alphaAttachmentThreshold);
} else {
var direction = MixDirection.mixOut;
if (drawOrder && Std.isOfType(timeline, DrawOrderTimeline) && timelineBlend == MixBlend.setup)
direction = MixDirection.mixIn;
timeline.apply(skeleton, animationLast, applyTime, events, alpha, timelineBlend, direction, false);
@ -419,12 +419,14 @@ class AnimationState {
* is not the last timeline to set the slot's attachment. In that case the timeline is applied only so subsequent
* timelines see any deform.
*/
public function applyAttachmentTimeline(timeline:AttachmentTimeline, skeleton:Skeleton, time:Float, blend:MixBlend, attachments:Bool) {
public function applyAttachmentTimeline(timeline:AttachmentTimeline, skeleton:Skeleton, time:Float, blend:MixBlend, out:Bool, attachments:Bool) {
var slot = skeleton.slots[timeline.slotIndex];
if (!slot.bone.active)
return;
if (time < timeline.frames[0]) { // Time is before first frame.
if (out) {
if (blend == MixBlend.setup) this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else if (time < timeline.frames[0]) { // Time is before first frame.
if (blend == MixBlend.setup || blend == MixBlend.first)
this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else
@ -889,6 +891,7 @@ class AnimationState {
} else if (to == null
|| Std.isOfType(timeline, AttachmentTimeline)
|| Std.isOfType(timeline, DrawOrderTimeline)
|| Std.isOfType(timeline, DrawOrderFolderTimeline)
|| Std.isOfType(timeline, EventTimeline)
|| !to.animation.hasTimeline(ids)) {
timelineMode[i] = FIRST;

View File

@ -0,0 +1,126 @@
/******************************************************************************
* 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 spine.Event;
import spine.Skeleton;
import spine.Slot;
/** Changes a subset of a skeleton's spine.Skeleton.drawOrder. */
class DrawOrderFolderTimeline extends Timeline {
private var slots:Array<Int>;
private var inFolder:Array<Bool>;
private var drawOrders:Array<Array<Int>>;
/** @param slots spine.Skeleton.slots indices controlled by this timeline, in setup order.
* @param slotCount The maximum number of slots in the skeleton. */
public function new(frameCount:Int, slots:Array<Int>, slotCount:Int) {
super(frameCount, Property.drawOrder);
this.slots = slots;
drawOrders = new Array<Array<Int>>();
drawOrders.resize(frameCount);
inFolder = new Array<Bool>();
inFolder.resize(slotCount);
for (i in 0...slotCount)
inFolder[i] = false;
for (i in slots)
inFolder[i] = true;
}
public var frameCount(get, never):Int;
private function get_frameCount():Int {
return frames.length;
}
/** The spine.Skeleton.slots indices that this timeline affects, in setup order. */
public function getSlots():Array<Int> {
return slots;
}
/** The draw order for each frame. See setFrame(). */
public function getDrawOrders():Array<Array<Int>> {
return drawOrders;
}
/** Sets the time and draw order for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds.
* @param drawOrder Ordered getSlots() indices, or null to use setup pose order. */
public function setFrame(frame:Int, time:Float, drawOrder:Array<Int>):Void {
frames[frame] = time;
drawOrders[frame] = drawOrder;
}
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend, direction:MixDirection,
appliedPose:Bool) {
if (direction == MixDirection.mixOut) {
if (blend == MixBlend.setup) setupApply(skeleton);
} else if (time < frames[0]) {
if (blend == MixBlend.setup || blend == MixBlend.first) setupApply(skeleton);
} else {
var order = drawOrders[Timeline.search1(frames, time)];
if (order == null)
setupApply(skeleton);
else
orderApply(skeleton, order);
}
}
private function setupApply(skeleton:Skeleton):Void {
var drawOrder = skeleton.drawOrder;
var allSlots = skeleton.slots;
var found = 0, done = slots.length;
var i = 0;
while (true) {
if (inFolder[drawOrder[i].data.index]) {
drawOrder[i] = allSlots[slots[found]];
found++;
if (found == done) break;
}
i++;
}
}
private function orderApply(skeleton:Skeleton, order:Array<Int>):Void {
var drawOrder = skeleton.drawOrder;
var allSlots = skeleton.slots;
var found = 0, done = slots.length;
var i = 0;
while (true) {
if (inFolder[drawOrder[i].data.index]) {
drawOrder[i] = allSlots[slots[order[found]]];
found++;
if (found == done) break;
}
i++;
}
}
}

View File

@ -53,7 +53,7 @@ class DrawOrderTimeline extends Timeline {
/** Sets the time and draw order for the specified frame.
* @param frame Between 0 and frameCount, inclusive.
* @param time The frame time in seconds.
* @param drawOrder For each slot in spine.Skeleton.slots, the index of the slot in the new draw order. May be null to use setup pose draw order. */
* @param drawOrder Ordered spine.Skeleton.slots indices, or null to use setup pose order. */
public function setFrame(frame:Int, time:Float, drawOrder:Array<Int>):Void {
frames[frame] = time;
drawOrders[frame] = drawOrder;

View File

@ -115,27 +115,21 @@ class IkConstraintTimeline extends CurveTimeline implements ConstraintTimeline {
softness = getBezierValue(time, i, SOFTNESS, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
}
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;
if (blend == 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;
}
} else {
pose.mix += (mix - pose.mix) * alpha;
pose.softness += (softness - pose.softness) * alpha;
if (direction == MixDirection.mixOut)
return;
}
pose.bendDirection = Std.int(frames[i + BEND_DIRECTION]);
pose.compress = frames[i + COMPRESS] != 0;

View File

@ -114,20 +114,15 @@ class PathConstraintMixTimeline extends CurveTimeline implements ConstraintTimel
y = getBezierValue(time, i, Y, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
}
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;
if (blend == 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;
} else {
pose.mixRotate += (rotate - pose.mixRotate) * alpha;
pose.mixX += (x - pose.mixX) * alpha;
pose.mixY += (y - pose.mixY) * alpha;
}
}
}

View File

@ -44,7 +44,7 @@ class PathConstraintSpacingTimeline extends ConstraintTimeline1 {
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);
pose.spacing = getAbsoluteValue(time, alpha, blend == MixBlend.add ? MixBlend.replace : blend, pose.spacing, constraint.data.setup.spacing);
}
}
}

View File

@ -33,6 +33,7 @@ package spine.animation;
class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity);
this.additive = true;
}
public function get(pose:PhysicsConstraintPose):Float {

View File

@ -35,6 +35,8 @@ import spine.Skeleton;
/** The base class for most spine.PhysicsConstraint timelines. */
abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
public var additive:Bool = false;
/**
* @param constraintIndex -1 for all physics constraints in the skeleton.
*/
@ -44,6 +46,7 @@ abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
public function apply(skeleton:Skeleton, lastTime:Float, time:Float, events:Array<Event>, alpha:Float, blend:MixBlend, direction:MixDirection,
appliedPose:Bool) {
if (blend == MixBlend.add && !additive) blend = MixBlend.replace;
if (constraintIndex == -1) {
var value:Float = time >= frames[0] ? getCurveValue(time) : 0;
for (constraint in skeleton.physics) {

View File

@ -33,6 +33,7 @@ package spine.animation;
class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
public function new(frameCount:Int, bezierCount:Int, constraintIndex:Int) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind);
this.additive = true;
}
public function get(pose:PhysicsConstraintPose):Float {

View File

@ -63,9 +63,10 @@ class RGB2Timeline extends SlotCurveTimeline {
public function apply1(slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var light:Color = pose.color, dark:Color = pose.darkColor;
var r:Float = 0, g:Float = 0, b:Float = 0, r2:Float = 0, g2:Float = 0, b2:Float = 0;
if (time < frames[0]) {
var setup = slot.data.setup;
var setupLight = setup.color, setupDark = setup.darkColor;
var setupLight:Color = setup.color, setupDark:Color = setup.darkColor;
switch (blend) {
case MixBlend.setup:
light.r = setupLight.r;
@ -74,76 +75,78 @@ class RGB2Timeline extends SlotCurveTimeline {
dark.r = setupDark.r;
dark.g = setupDark.g;
dark.b = setupDark.b;
return;
case MixBlend.first:
light.r += (setupLight.r - light.r) * alpha;
light.g += (setupLight.g - light.g) * alpha;
light.b += (setupLight.b - light.b) * alpha;
dark.r += (setupDark.r - dark.r) * alpha;
dark.g += (setupDark.g - dark.g) * alpha;
dark.b += (setupDark.b - dark.b) * alpha;
r = light.r + (setupLight.r - light.r) * alpha;
g = light.g + (setupLight.g - light.g) * alpha;
b = light.b + (setupLight.b - light.b) * alpha;
r2 = dark.r + (setupDark.r - dark.r) * alpha;
g2 = dark.g + (setupDark.g - dark.g) * alpha;
b2 = dark.b + (setupDark.b - dark.b) * alpha;
default:
return;
}
return;
}
var r:Float = 0, g:Float = 0, b:Float = 0, a:Float = 0, r2:Float = 0, g2:Float = 0, b2:Float = 0;
var i:Int = Timeline.search(frames, time, ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
var t:Float = (time - before) / (frames[i + ENTRIES] - before);
r += (frames[i + ENTRIES + R] - r) * t;
g += (frames[i + ENTRIES + G] - g) * t;
b += (frames[i + ENTRIES + B] - b) * t;
r2 += (frames[i + ENTRIES + R2] - r2) * t;
g2 += (frames[i + ENTRIES + G2] - g2) * t;
b2 += (frames[i + ENTRIES + B2] - b2) * t;
case CurveTimeline.STEPPED:
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
default:
r = getBezierValue(time, i, R, curveType - CurveTimeline.BEZIER);
g = getBezierValue(time, i, G, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
b = getBezierValue(time, i, B, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
r2 = getBezierValue(time, i, R2, curveType + CurveTimeline.BEZIER_SIZE * 3 - CurveTimeline.BEZIER);
g2 = getBezierValue(time, i, G2, curveType + CurveTimeline.BEZIER_SIZE * 4 - CurveTimeline.BEZIER);
b2 = getBezierValue(time, i, B2, curveType + CurveTimeline.BEZIER_SIZE * 5 - CurveTimeline.BEZIER);
}
if (alpha == 1) {
light.r = r;
light.g = g;
light.b = b;
dark.r = r2;
dark.g = g2;
dark.b = b2;
} else {
if (blend == MixBlend.setup) {
var setup = slot.data.setup;
var setupLight = setup.color, setupDark = setup.darkColor;
light.r = setupLight.r;
light.g = setupLight.g;
light.b = setupLight.b;
dark.r = setupDark.r;
dark.g = setupDark.g;
dark.b = setupDark.b;
var i:Int = Timeline.search(frames, time, ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
var t:Float = (time - before) / (frames[i + ENTRIES] - before);
r += (frames[i + ENTRIES + R] - r) * t;
g += (frames[i + ENTRIES + G] - g) * t;
b += (frames[i + ENTRIES + B] - b) * t;
r2 += (frames[i + ENTRIES + R2] - r2) * t;
g2 += (frames[i + ENTRIES + G2] - g2) * t;
b2 += (frames[i + ENTRIES + B2] - b2) * t;
case CurveTimeline.STEPPED:
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
default:
r = getBezierValue(time, i, R, curveType - CurveTimeline.BEZIER);
g = getBezierValue(time, i, G, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
b = getBezierValue(time, i, B, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
r2 = getBezierValue(time, i, R2, curveType + CurveTimeline.BEZIER_SIZE * 3 - CurveTimeline.BEZIER);
g2 = getBezierValue(time, i, G2, curveType + CurveTimeline.BEZIER_SIZE * 4 - CurveTimeline.BEZIER);
b2 = getBezierValue(time, i, B2, curveType + CurveTimeline.BEZIER_SIZE * 5 - CurveTimeline.BEZIER);
}
if (alpha != 1) {
if (blend == MixBlend.setup) {
var setupPose = slot.data.setup;
var setup = setupPose.color;
r = setup.r + (r - setup.r) * alpha;
g = setup.g + (g - setup.g) * alpha;
b = setup.b + (b - setup.b) * alpha;
setup = setupPose.darkColor;
r2 = setup.r + (r2 - setup.r) * alpha;
g2 = setup.g + (g2 - setup.g) * alpha;
b2 = setup.b + (b2 - setup.b) * alpha;
} else {
r = light.r + (r - light.r) * alpha;
g = light.g + (g - light.g) * alpha;
b = light.b + (b - light.b) * alpha;
r2 = dark.r + (r2 - dark.r) * alpha;
g2 = dark.g + (g2 - dark.g) * alpha;
b2 = dark.b + (b2 - dark.b) * alpha;
}
}
light.r += (r - light.r) * alpha;
light.g += (g - light.g) * alpha;
light.b += (b - light.b) * alpha;
dark.r += (r2 - dark.r) * alpha;
dark.g += (g2 - dark.g) * alpha;
dark.b += (b2 - dark.b) * alpha;
}
light.r = r < 0 ? 0 : (r > 1 ? 1 : r);
light.g = g < 0 ? 0 : (g > 1 ? 1 : g);
light.b = b < 0 ? 0 : (b > 1 ? 1 : b);
dark.r = r2 < 0 ? 0 : (r2 > 1 ? 1 : r2);
dark.g = g2 < 0 ? 0 : (g2 > 1 ? 1 : g2);
dark.b = b2 < 0 ? 0 : (b2 > 1 ? 1 : b2);
}
}

View File

@ -71,6 +71,7 @@ class RGBA2Timeline extends SlotCurveTimeline {
public function apply1(slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var light = pose.color, dark = pose.darkColor;
var r2:Float = 0, g2:Float = 0, b2:Float = 0;
if (time < frames[0]) {
var setup = slot.data.setup;
var setupLight = setup.color, setupDark = setup.darkColor;
@ -80,70 +81,76 @@ class RGBA2Timeline extends SlotCurveTimeline {
dark.r = setupDark.r;
dark.g = setupDark.g;
dark.b = setupDark.b;
return;
case MixBlend.first:
light.add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha,
(setupLight.a - light.a) * alpha);
dark.r += (setupDark.r - dark.r) * alpha;
dark.g += (setupDark.g - dark.g) * alpha;
dark.b += (setupDark.b - dark.b) * alpha;
r2 = dark.r + (setupDark.r - dark.r) * alpha;
g2 = dark.g + (setupDark.g - dark.g) * alpha;
b2 = dark.b + (setupDark.b - dark.b) * alpha;
default:
return;
}
return;
}
var r:Float = 0, g:Float = 0, b:Float = 0, a:Float = 0, r2:Float = 0, g2:Float = 0, b2:Float = 0;
var i:Int = Timeline.search(frames, time, ENTRIES);
var curveType:Int = Std.int(curves[i >> 3]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
a = frames[i + A];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
var t:Float = (time - before) / (frames[i + ENTRIES] - before);
r += (frames[i + ENTRIES + R] - r) * t;
g += (frames[i + ENTRIES + G] - g) * t;
b += (frames[i + ENTRIES + B] - b) * t;
a += (frames[i + ENTRIES + A] - a) * t;
r2 += (frames[i + ENTRIES + R2] - r2) * t;
g2 += (frames[i + ENTRIES + G2] - g2) * t;
b2 += (frames[i + ENTRIES + B2] - b2) * t;
case CurveTimeline.STEPPED:
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
a = frames[i + A];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
default:
r = getBezierValue(time, i, R, curveType - CurveTimeline.BEZIER);
g = getBezierValue(time, i, G, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
b = getBezierValue(time, i, B, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
a = getBezierValue(time, i, A, curveType + CurveTimeline.BEZIER_SIZE * 3 - CurveTimeline.BEZIER);
r2 = getBezierValue(time, i, R2, curveType + CurveTimeline.BEZIER_SIZE * 4 - CurveTimeline.BEZIER);
g2 = getBezierValue(time, i, G2, curveType + CurveTimeline.BEZIER_SIZE * 5 - CurveTimeline.BEZIER);
b2 = getBezierValue(time, i, B2, curveType + CurveTimeline.BEZIER_SIZE * 6 - CurveTimeline.BEZIER);
}
if (alpha == 1) {
light.set(r, g, b, a);
dark.r = r2;
dark.g = g2;
dark.b = b2;
} else {
if (blend == MixBlend.setup) {
var setup = slot.data.setup;
light.setFromColor(setup.color);
dark.setFromColor(setup.darkColor);
var r:Float = 0, g:Float = 0, b:Float = 0, a:Float = 0;
var i:Int = Timeline.search(frames, time, ENTRIES);
var curveType:Int = Std.int(curves[i >> 3]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
a = frames[i + A];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
var t:Float = (time - before) / (frames[i + ENTRIES] - before);
r += (frames[i + ENTRIES + R] - r) * t;
g += (frames[i + ENTRIES + G] - g) * t;
b += (frames[i + ENTRIES + B] - b) * t;
a += (frames[i + ENTRIES + A] - a) * t;
r2 += (frames[i + ENTRIES + R2] - r2) * t;
g2 += (frames[i + ENTRIES + G2] - g2) * t;
b2 += (frames[i + ENTRIES + B2] - b2) * t;
case CurveTimeline.STEPPED:
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
a = frames[i + A];
r2 = frames[i + R2];
g2 = frames[i + G2];
b2 = frames[i + B2];
default:
r = getBezierValue(time, i, R, curveType - CurveTimeline.BEZIER);
g = getBezierValue(time, i, G, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
b = getBezierValue(time, i, B, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
a = getBezierValue(time, i, A, curveType + CurveTimeline.BEZIER_SIZE * 3 - CurveTimeline.BEZIER);
r2 = getBezierValue(time, i, R2, curveType + CurveTimeline.BEZIER_SIZE * 4 - CurveTimeline.BEZIER);
g2 = getBezierValue(time, i, G2, curveType + CurveTimeline.BEZIER_SIZE * 5 - CurveTimeline.BEZIER);
b2 = getBezierValue(time, i, B2, curveType + CurveTimeline.BEZIER_SIZE * 6 - CurveTimeline.BEZIER);
}
if (alpha == 1)
light.set(r, g, b, a);
else if (blend == MixBlend.setup) {
var setupPose = slot.data.setup;
var setup = setupPose.color;
light.set(setup.r + (r - setup.r) * alpha, setup.g + (g - setup.g) * alpha, setup.b + (b - setup.b) * alpha,
setup.a + (a - setup.a) * alpha);
setup = setupPose.darkColor;
r2 = setup.r + (r2 - setup.r) * alpha;
g2 = setup.g + (g2 - setup.g) * alpha;
b2 = setup.b + (b2 - setup.b) * alpha;
} else {
light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha);
r2 = dark.r + (r2 - dark.r) * alpha;
g2 = dark.g + (g2 - dark.g) * alpha;
b2 = dark.b + (b2 - dark.b) * alpha;
}
light.add((r - light.r) * alpha, (g - light.g) * alpha, (b - light.b) * alpha, (a - light.a) * alpha);
dark.r += (r2 - dark.r) * alpha;
dark.g += (g2 - dark.g) * alpha;
dark.b += (b2 - dark.b) * alpha;
}
dark.r = r2 < 0 ? 0 : (r2 > 1 ? 1 : r2);
dark.g = g2 < 0 ? 0 : (g2 > 1 ? 1 : g2);
dark.b = b2 < 0 ? 0 : (b2 > 1 ? 1 : b2);
}
}

View File

@ -97,12 +97,15 @@ class RGBATimeline extends SlotCurveTimeline {
a = getBezierValue(time, i, A, curveType + CurveTimeline.BEZIER_SIZE * 3 - CurveTimeline.BEZIER);
}
if (alpha == 1) {
if (alpha == 1)
color.set(r, g, b, a);
} else {
if (blend == MixBlend.setup)
color.setFromColor(slot.data.setup.color);
color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
else {
if (blend == MixBlend.setup) {
var setup = slot.data.setup.color;
color.set(setup.r + (r - setup.r) * alpha, setup.g + (g - setup.g) * alpha, setup.b + (b - setup.b) * alpha,
setup.a + (a - setup.a) * alpha);
} else
color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha);
}
}
}

View File

@ -57,6 +57,7 @@ class RGBTimeline extends SlotCurveTimeline {
public function apply1(slot:Slot, pose:SlotPose, time:Float, alpha:Float, blend:MixBlend) {
var color = pose.color;
var r:Float = 0, g:Float = 0, b:Float = 0;
if (time < frames[0]) {
var setup = slot.data.setup.color;
switch (blend) {
@ -64,50 +65,51 @@ class RGBTimeline extends SlotCurveTimeline {
color.r = setup.r;
color.g = setup.g;
color.b = setup.b;
return;
case MixBlend.first:
color.r += (setup.r - color.r) * alpha;
color.g += (setup.g - color.g) * alpha;
color.b += (setup.b - color.b) * alpha;
r = color.r + (setup.r - color.r) * alpha;
g = color.g + (setup.g - color.g) * alpha;
b = color.b + (setup.b - color.b) * alpha;
default:
return;
}
return;
}
var r:Float = 0, g:Float = 0, b:Float = 0;
var i:Int = Timeline.search(frames, time, ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
var t:Float = (time - before) / (frames[i + ENTRIES] - before);
r += (frames[i + ENTRIES + R] - r) * t;
g += (frames[i + ENTRIES + G] - g) * t;
b += (frames[i + ENTRIES + B] - b) * t;
case CurveTimeline.STEPPED:
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
default:
r = getBezierValue(time, i, R, curveType - CurveTimeline.BEZIER);
g = getBezierValue(time, i, G, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
b = getBezierValue(time, i, B, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
}
if (alpha == 1) {
color.r = r;
color.g = g;
color.b = b;
} else {
if (blend == MixBlend.setup) {
var setup = slot.data.setup.color;
color.r = setup.r;
color.g = setup.g;
color.b = setup.b;
var i:Int = Timeline.search(frames, time, ENTRIES);
var curveType:Int = Std.int(curves[Std.int(i / ENTRIES)]);
switch (curveType) {
case CurveTimeline.LINEAR:
var before:Float = frames[i];
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
var t:Float = (time - before) / (frames[i + ENTRIES] - before);
r += (frames[i + ENTRIES + R] - r) * t;
g += (frames[i + ENTRIES + G] - g) * t;
b += (frames[i + ENTRIES + B] - b) * t;
case CurveTimeline.STEPPED:
r = frames[i + R];
g = frames[i + G];
b = frames[i + B];
default:
r = getBezierValue(time, i, R, curveType - CurveTimeline.BEZIER);
g = getBezierValue(time, i, G, curveType + CurveTimeline.BEZIER_SIZE - CurveTimeline.BEZIER);
b = getBezierValue(time, i, B, curveType + CurveTimeline.BEZIER_SIZE * 2 - CurveTimeline.BEZIER);
}
if (alpha != 1) {
if (blend == MixBlend.setup) {
var setup = slot.data.setup.color;
r = setup.r + (r - setup.r) * alpha;
g = setup.g + (g - setup.g) * alpha;
b = setup.b + (b - setup.b) * alpha;
} else {
r = color.r + (r - color.r) * alpha;
g = color.g + (g - color.g) * alpha;
b = color.b + (b - color.b) * alpha;
}
}
color.r += (r - color.r) * alpha;
color.g += (g - color.g) * alpha;
color.b += (b - color.b) * alpha;
}
color.r = r < 0 ? 0 : (r > 1 ? 1 : r);
color.g = g < 0 ? 0 : (g > 1 ? 1 : g);
color.b = b < 0 ? 0 : (b > 1 ? 1 : b);
}
}

View File

@ -29,7 +29,6 @@
package spine.animation;
import spine.attachments.VertexAttachment;
import spine.attachments.Attachment;
/** Changes a slot's Slot#getSequenceIndex() for an attachment's Sequence. */
@ -39,9 +38,9 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
static var DELAY = 2;
var slotIndex:Int;
var attachment:HasTextureRegion;
var attachment:HasSequence;
public function new(frameCount:Int, slotIndex:Int, attachment:HasTextureRegion) {
public function new(frameCount:Int, slotIndex:Int, attachment:HasSequence) {
super(frameCount, Std.string(Property.sequence) + "|" + Std.string(slotIndex) + "|" + Std.string(attachment.sequence.id));
this.slotIndex = slotIndex;
this.attachment = attachment;
@ -55,6 +54,8 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
return this.slotIndex;
}
/** The attachment for which the sequenceIndex will be set.
* See VertexAttachment.timelineAttachment. */
public function getAttachment():Attachment {
return cast(attachment, Attachment);
}
@ -76,12 +77,11 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
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)
return;
}
var slotAttachment:Attachment = cast(pose.attachment, Attachment);
var attachmentRef:Attachment = cast(this.attachment, Attachment);
if (!Std.isOfType(slotAttachment, HasSequence) || slotAttachment.timelineAttachment != attachmentRef)
return;
if (direction == MixDirection.mixOut) {
if (blend == MixBlend.setup)
@ -100,10 +100,8 @@ class SequenceTimeline extends Timeline implements SlotTimeline {
var modeAndIndex = Std.int(frames[i + SequenceTimeline.MODE]);
var delay = frames[i + SequenceTimeline.DELAY];
if (this.attachment.sequence == null)
return;
var index = modeAndIndex >> 4,
count = this.attachment.sequence.regions.length;
var hasSeq:HasSequence = cast(slotAttachment, HasSequence);
var index = modeAndIndex >> 4, count = hasSeq.sequence.regions.length;
var mode = SequenceMode.values[modeAndIndex & 0xf];
if (mode != SequenceMode.hold) {
index += Std.int(((time - before) / delay + 0.00001));

View File

@ -30,13 +30,10 @@
package spine.attachments;
import spine.atlas.TextureAtlas;
import spine.atlas.TextureAtlasRegion;
import spine.Skin;
import spine.Sequence;
/**
* The interface which can be implemented to customize creating and populating attachments.
*
* @see https://esotericsoftware.com/spine-loading-skeleton-data#AttachmentLoader Loading skeleton data in the Spine Runtimes Guide
*/
class AtlasAttachmentLoader implements AttachmentLoader {
private var atlas:TextureAtlas;
@ -50,72 +47,41 @@ class AtlasAttachmentLoader implements AttachmentLoader {
this.allowMissingRegions = allowMissingRegions;
}
private function loadSequence(name:String, basePath:String, sequence:Sequence) {
private function findRegions(name:String, basePath:String, sequence:Sequence):Void {
var regions = sequence.regions;
for (i in 0...regions.length) {
var path = sequence.getPath(basePath, i);
regions[i] = this.atlas.findRegion(path);
if (regions[i] == null)
throw new SpineException("Region not found in atlas: " + path + " (sequence: " + name + ")");
}
for (i in 0...regions.length)
regions[i] = findRegion(name, sequence.getPath(basePath, i));
}
private function findRegion(name:String, path:String):TextureAtlasRegion {
var region = atlas.findRegion(path);
if (region == null && !allowMissingRegions)
throw new SpineException("Region not found in atlas: " + path + " (attachment: " + name + ")");
return region;
}
/**
* @return May be null to not load the attachment.
*/
public function newRegionAttachment(skin:Skin, name:String, path:String, sequence:Sequence):RegionAttachment {
var attachment = new RegionAttachment(name, path);
if (sequence != null) {
this.loadSequence(name, path, sequence);
} else {
var region = this.atlas.findRegion(path);
if (region == null && !this.allowMissingRegions)
throw new SpineException("Region not found in atlas: " + path + " (region attachment: " + name + ")");
attachment.region = region;
}
return attachment;
findRegions(name, path, sequence);
return new RegionAttachment(name, sequence);
}
/**
* @return May be null to not load the attachment. In that case null should also be returned for child meshes.
*/
public function newMeshAttachment(skin:Skin, name:String, path:String, sequence:Sequence):MeshAttachment {
var attachment = new MeshAttachment(name, path);
if (sequence != null) {
this.loadSequence(name, path, sequence);
} else {
var region = atlas.findRegion(path);
if (region == null && !this.allowMissingRegions)
throw new SpineException("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
attachment.region = region;
}
return attachment;
findRegions(name, path, sequence);
return new MeshAttachment(name, sequence);
}
/**
* @return May be null to not load the attachment.
*/
public function newBoundingBoxAttachment(skin:Skin, name:String):BoundingBoxAttachment {
return new BoundingBoxAttachment(name);
}
/**
* @return May be null to not load the attachment.
*/
public function newPathAttachment(skin:Skin, name:String):PathAttachment {
return new PathAttachment(name);
}
/**
* @return May be null to not load the attachment.
*/
public function newPointAttachment(skin:Skin, name:String):PointAttachment {
return new PointAttachment(name);
}
/**
* @return May be null to not load the attachment.
*/
public function newClippingAttachment(skin:Skin, name:String):ClippingAttachment {
return new ClippingAttachment(name);
}

View File

@ -33,11 +33,16 @@ package spine.attachments;
class Attachment {
private var _name:String;
/** 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:Attachment;
public function new(name:String) {
if (name == null) {
throw new SpineException("name cannot be null.");
}
_name = name;
timelineAttachment = this;
}
/** The attachment's name. */

View File

@ -30,6 +30,9 @@
package spine.attachments;
import spine.Color;
import spine.Sequence;
import spine.TextureRegion;
import spine.HasSequence;
import spine.atlas.TextureAtlasRegion;
import spine.atlas.TextureAtlasPage;
@ -37,60 +40,114 @@ import spine.atlas.TextureAtlasPage;
* supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
*
* @see https://esotericsoftware.com/spine-meshes Mesh attachments in the Spine User Guide */
class MeshAttachment extends VertexAttachment implements HasTextureRegion {
public var region:TextureRegion;
public var path:String;
class MeshAttachment extends VertexAttachment implements HasSequence {
public var sequence:Sequence;
/** The UV pair for each vertex, normalized within the texture region. */
public var regionUVs = new Array<Float>();
/** The UV pair for each vertex, normalized within the entire texture.
* See #updateRegion(). */
public var uvs = new Array<Float>();
/** Triplets of vertex indices which describe the mesh's triangulation. */
public var triangles = new Array<Int>();
/** The number of entries at the beginning of #vertices that make up the mesh hull. */
public var hullLength:Int = 0;
/** The name of the texture region for this attachment. */
public var path:String;
/** The color to tint the mesh. */
public var color:Color = new Color(1, 1, 1, 1);
/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the #bones, #vertices,
* #regionUVs, #triangles, #hullLength, #edges, #width, and #height with the
* parent mesh, but may have a different #name or #path (and therefore a different texture). */
private var _parentMesh:MeshAttachment;
/** Vertex index pairs describing edges for controlling triangulation, or null if nonessential data was not exported. Mesh
* triangles never cross edges. Triangulation is not performed at runtime. */
public var edges = new Array<Int>();
/** The width of the mesh's image, or zero if nonessential data was not exported. */
public var width:Float = 0;
/** The height of the mesh's image, or zero if nonessential data was not exported. */
public var height:Float = 0;
/** The number of entries at the beginning of #vertices that make up the mesh hull. */
public var hullLength:Int = 0;
/** Vertex index pairs describing edges for controlling triangulation, or null if nonessential data was not exported. Mesh
* triangles will never cross edges. Triangulation is not performed at runtime. */
public var edges = new Array<Int>();
public var rendererObject:Dynamic;
public var sequence:Sequence;
/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the #bones, #vertices,
* #regionUVs, #triangles, #hullLength, #edges, #width, and #height with the
* parent mesh, but may have a different #name or #path (and therefore a different texture). */
private var _parentMesh:MeshAttachment;
/** Copy constructor. Use newLinkedMesh() if the other mesh is a linked mesh. */
public function new(name:String, path:String) {
public function new(name:String, sequence:Sequence) {
super(name);
this.path = path;
this.sequence = sequence;
}
/** Calculates uvs using the regionUVs and region. Must be called if the region, the region's properties, or
* the regionUVs are changed. */
public function updateRegion():Void {
if (region == null) {
throw new SpineException("Region not set.");
return;
override public function copy():Attachment {
if (_parentMesh != null)
return newLinkedMesh();
var copy = new MeshAttachment(name, sequence.copy());
copy.path = path;
copy.color.setFromColor(color);
copy.rendererObject = rendererObject;
this.copyTo(copy);
copy.regionUVs = regionUVs.copy();
copy.triangles = triangles.copy();
copy.hullLength = hullLength;
if (edges != null) {
copy.edges = edges.copy();
}
var regionUVs = this.regionUVs;
if (uvs.length != regionUVs.length) {
uvs = new Array<Float>();
uvs.resize(regionUVs.length);
copy.width = width;
copy.height = height;
return copy;
}
/** Calls Sequence.update() on this attachment's sequence. */
public function updateSequence():Void {
sequence.update(this);
}
public var parentMesh(get, set):MeshAttachment;
private function get_parentMesh():MeshAttachment {
return _parentMesh;
}
private function set_parentMesh(parentMesh:MeshAttachment):MeshAttachment {
_parentMesh = parentMesh;
if (parentMesh != null) {
bones = parentMesh.bones;
vertices = parentMesh.vertices;
worldVerticesLength = parentMesh.worldVerticesLength;
regionUVs = parentMesh.regionUVs;
triangles = parentMesh.triangles;
hullLength = parentMesh.hullLength;
edges = parentMesh.edges;
width = parentMesh.width;
height = parentMesh.height;
}
return _parentMesh;
}
/** Returns a new mesh with the parentMesh set to this mesh's parent mesh, if any, else to this mesh. */
public function newLinkedMesh():MeshAttachment {
var copy = new MeshAttachment(name, sequence.copy());
copy.rendererObject = rendererObject;
copy.timelineAttachment = timelineAttachment;
copy.path = path;
copy.color.setFromColor(color);
copy.parentMesh = _parentMesh != null ? _parentMesh : this;
copy.updateSequence();
return copy;
}
/** Computes UVs for a mesh attachment.
* @param uvs Output array for the computed UVs, same length as regionUVs. */
public static function computeUVs(region:TextureRegion, regionUVs:Array<Float>, uvs:Array<Float>):Void {
if (region == null) {
throw "Region not set.";
return;
}
var n = uvs.length;
var u = region.u, v = region.v, width:Float = 0, height:Float = 0;
@ -145,8 +202,8 @@ class MeshAttachment extends VertexAttachment implements HasTextureRegion {
u = v = 0;
width = height = 1;
} else {
width = this.region.u2 - u;
height = this.region.v2 - v;
width = region.u2 - u;
height = region.v2 - v;
}
var i = 0;
while (i < n) {
@ -155,72 +212,4 @@ class MeshAttachment extends VertexAttachment implements HasTextureRegion {
i += 2;
}
}
public var parentMesh(get, set):MeshAttachment;
private function get_parentMesh():MeshAttachment {
return _parentMesh;
}
private function set_parentMesh(parentMesh:MeshAttachment):MeshAttachment {
_parentMesh = parentMesh;
if (parentMesh != null) {
bones = parentMesh.bones;
vertices = parentMesh.vertices;
worldVerticesLength = parentMesh.worldVerticesLength;
regionUVs = parentMesh.regionUVs;
triangles = parentMesh.triangles;
hullLength = parentMesh.hullLength;
edges = parentMesh.edges;
width = parentMesh.width;
height = parentMesh.height;
}
return _parentMesh;
}
override public function copy():Attachment {
if (parentMesh != null)
return newLinkedMesh();
var copy:MeshAttachment = new MeshAttachment(name, this.path);
copy.region = region;
copy.color.setFromColor(color);
copy.rendererObject = rendererObject;
this.copyTo(copy);
copy.regionUVs = regionUVs.copy();
copy.uvs = uvs.copy();
copy.triangles = triangles.copy();
copy.hullLength = hullLength;
copy.sequence = sequence != null ? sequence.copy() : null;
if (edges != null) {
copy.edges = edges.copy();
}
copy.width = width;
copy.height = height;
return copy;
}
/** If the attachment has a sequence, the region may be changed. */
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.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. */
public function newLinkedMesh():MeshAttachment {
var copy:MeshAttachment = new MeshAttachment(name, path);
copy.rendererObject = rendererObject;
copy.region = region;
copy.color.setFromColor(color);
copy.timelineAttachment = timelineAttachment;
copy.parentMesh = this.parentMesh != null ? this.parentMesh : this;
if (copy.region != null)
copy.updateRegion();
return copy;
}
}

View File

@ -30,12 +30,18 @@
package spine.attachments;
import spine.Color;
import spine.Sequence;
import spine.SlotPose;
import spine.Slot;
import spine.TextureRegion;
import spine.MathUtils;
import spine.HasSequence;
/** An attachment that displays a textured quadrilateral.
*
* @see https://esotericsoftware.com/spine-regions Region attachments in the Spine User Guide
*/
class RegionAttachment extends Attachment implements HasTextureRegion {
class RegionAttachment extends Attachment implements HasSequence {
public static inline var BLX:Int = 0;
public static inline var BLY:Int = 1;
public static inline var ULX:Int = 2;
@ -45,6 +51,8 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
public static inline var BRX:Int = 6;
public static inline var BRY:Int = 7;
public var sequence:Sequence;
/** The local x translation. */
public var x:Float = 0;
@ -66,31 +74,86 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
/** The height of the region attachment in Spine. */
public var height:Float = 0;
public var color:Color = new Color(1, 1, 1, 1);
/** The name of the texture region for this attachment. */
public var path:String;
/** The color to tint the region attachment. */
public var color:Color = new Color(1, 1, 1, 1);
public var rendererObject:Dynamic;
public var region:TextureRegion;
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 offset: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.
*/
public function new(name:String, path:String) {
public function new(name:String, sequence:Sequence) {
super(name);
this.path = path;
this.sequence = sequence;
}
/** Calculates the RegionAttachment.offsets and RegionAttachment.uvs using the region and the attachment's transform. Must be called if the
* region, the region's properties, or the transform are changed. */
public function updateRegion():Void {
override public function copy():Attachment {
var copy = new RegionAttachment(name, sequence.copy());
copy.path = path;
copy.x = x;
copy.y = y;
copy.scaleX = scaleX;
copy.scaleY = scaleY;
copy.rotation = rotation;
copy.width = width;
copy.height = height;
copy.color.setFromColor(color);
return copy;
}
/** Transforms the attachment's four vertices to world coordinates.
*
* @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 vertexOffsets The vertex offsets from the sequence.
* @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, vertexOffsets:Array<Float>, worldVertices:Array<Float>, offset:Int, stride:Int):Void {
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 = vertexOffsets[0];
var offsetY = vertexOffsets[1];
worldVertices[offset] = offsetX * a + offsetY * b + x; // br
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffsets[2];
offsetY = vertexOffsets[3];
worldVertices[offset] = offsetX * a + offsetY * b + x; // bl
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffsets[4];
offsetY = vertexOffsets[5];
worldVertices[offset] = offsetX * a + offsetY * b + x; // ul
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffsets[6];
offsetY = vertexOffsets[7];
worldVertices[offset] = offsetX * a + offsetY * b + x; // ur
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
}
/** Returns the vertex offsets for the given slot pose. */
public function getOffsets(pose:SlotPose):Array<Float> {
return sequence.offsets[sequence.resolveIndex(pose)];
}
/** Calls Sequence.update() on this attachment's sequence. */
public function updateSequence():Void {
sequence.update(this);
}
/** Computes UVs and offsets for a region attachment.
* @param uvs Output array for the computed UVs, length of 8.
* @param offset Output array for the computed vertex offsets, length of 8. */
public static function computeUVs(region:TextureRegion, x:Float, y:Float, scaleX:Float, scaleY:Float, rotation:Float,
width:Float, height:Float, offset:Array<Float>, uvs:Array<Float>):Void {
if (region == null) throw "Region not set.";
var regionScaleX = width / region.originalWidth * scaleX;
var regionScaleY = height / region.originalHeight * scaleY;
var localX = -width / 2 * scaleX + region.offsetX * regionScaleX;
@ -100,7 +163,6 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
var radians = rotation * MathUtils.degRad;
var cos = Math.cos(radians);
var sin = Math.sin(radians);
var x = this.x, y = this.y;
var localXCos = localX * cos + x;
var localXSin = localX * sin;
var localYCos = localY * cos + y;
@ -109,7 +171,6 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
var localX2Sin = localX2 * sin;
var localY2Cos = localY2 * cos + y;
var localY2Sin = localY2 * sin;
offset[0] = localXCos - localYSin;
offset[1] = localYCos + localXSin;
offset[2] = localXCos - localY2Sin;
@ -128,83 +189,22 @@ class RegionAttachment extends Attachment implements HasTextureRegion {
uvs[5] = 1;
uvs[6] = 1;
uvs[7] = 0;
} else if (region.degrees == 90) {
uvs[0] = region.u2;
uvs[1] = region.v2;
uvs[2] = region.u;
uvs[3] = region.v2;
uvs[4] = region.u;
uvs[5] = region.v;
uvs[6] = region.u2;
uvs[7] = region.v;
} else {
uvs[0] = region.u;
uvs[1] = region.v2;
uvs[2] = region.u;
uvs[3] = region.v;
uvs[4] = region.u2;
uvs[5] = region.v;
uvs[6] = region.u2;
uvs[7] = region.v2;
if (region.degrees == 90) {
uvs[0] = region.u2;
uvs[3] = region.v2;
uvs[4] = region.u;
uvs[7] = region.v;
} else {
uvs[0] = region.u;
uvs[3] = region.v;
uvs[4] = region.u2;
uvs[7] = region.v2;
}
}
}
/** 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.applied, this);
var vertexOffset = this.offset;
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;
offsetX = vertexOffset[0];
offsetY = vertexOffset[1];
worldVertices[offset] = offsetX * a + offsetY * b + x; // br
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffset[2];
offsetY = vertexOffset[3];
worldVertices[offset] = offsetX * a + offsetY * b + x; // bl
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffset[4];
offsetY = vertexOffset[5];
worldVertices[offset] = offsetX * a + offsetY * b + x; // ul
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
offset += stride;
offsetX = vertexOffset[6];
offsetY = vertexOffset[7];
worldVertices[offset] = offsetX * a + offsetY * b + x; // ur
worldVertices[offset + 1] = offsetX * c + offsetY * d + y;
}
override public function copy():Attachment {
var copy:RegionAttachment = new RegionAttachment(name, path);
copy.region = region;
copy.rendererObject = rendererObject;
copy.x = x;
copy.y = y;
copy.scaleX = scaleX;
copy.scaleY = scaleY;
copy.rotation = rotation;
copy.width = width;
copy.height = height;
copy.uvs = uvs.copy();
copy.offset = offset.copy();
copy.color.setFromColor(color);
copy.sequence = sequence != null ? sequence.copy() : null;
return copy;
}
}

View File

@ -55,13 +55,8 @@ class VertexAttachment extends Attachment {
/** 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;
public function new(name:String) {
super(name);
timelineAttachment = this;
}
/** Transforms the attachment's local vertices to world coordinates. If the slot's spine.SlotPose.deform is

View File

@ -174,12 +174,14 @@ class SkeletonSprite extends FlxTypedGroup<FlxObject> {
numFloats = clippedVertexSize << 2;
if (numFloats > worldVertices.length)
worldVertices.resize(numFloats);
region.computeWorldVertices(slot, worldVertices, 0, clippedVertexSize);
var sequence = region.sequence;
var sequenceIndex = sequence.resolveIndex(pose);
region.computeWorldVertices(slot, sequence.offsets[sequenceIndex], worldVertices, 0, clippedVertexSize);
mesh = getFlixelMeshFromRendererAttachment(region);
mesh.graphic = region.region.texture;
mesh.graphic = sequence.regions[sequenceIndex].texture;
triangles = QUAD_INDICES;
uvs = region.uvs;
uvs = sequence.getUVs(sequenceIndex);
attachmentColor = region.color;
} else if (Std.isOfType(attachment, MeshAttachment)) {
var meshAttachment:MeshAttachment = cast(attachment, MeshAttachment);
@ -190,10 +192,12 @@ class SkeletonSprite extends FlxTypedGroup<FlxObject> {
}
meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, worldVertices, 0, clippedVertexSize);
var sequence = meshAttachment.sequence;
var sequenceIndex = sequence.resolveIndex(pose);
mesh = getFlixelMeshFromRendererAttachment(meshAttachment);
mesh.graphic = meshAttachment.region.texture;
mesh.graphic = sequence.regions[sequenceIndex].texture;
triangles = meshAttachment.triangles;
uvs = meshAttachment.uvs;
uvs = sequence.getUVs(sequenceIndex);
attachmentColor = meshAttachment.color;
} else if (Std.isOfType(attachment, ClippingAttachment)) {
var clip:ClippingAttachment = cast(attachment, ClippingAttachment);
@ -553,5 +557,4 @@ class SkeletonSprite extends FlxTypedGroup<FlxObject> {
typedef RenderedAttachment = {
var rendererObject:Dynamic;
var region:TextureRegion;
}

View File

@ -127,15 +127,18 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
verticesCount = verticesLength >> 1;
if (worldVertices.length < verticesLength)
worldVertices.resize(verticesLength);
region.computeWorldVertices(slot, worldVertices, 0, 2);
var sequence = region.sequence;
var sequenceIndex = sequence.resolveIndex(pose);
region.computeWorldVertices(slot, sequence.offsets[sequenceIndex], worldVertices, 0, 2);
mesh = null;
var regionTexture = sequence.regions[sequenceIndex].texture;
if (Std.isOfType(region.rendererObject, SkeletonMesh)) {
mesh = cast(region.rendererObject, SkeletonMesh);
mesh.texture = region.region.texture;
mesh.texture = regionTexture;
indices = QUAD_INDICES;
} else {
mesh = region.rendererObject = new SkeletonMesh(cast(region.region.texture, Texture));
mesh = region.rendererObject = new SkeletonMesh(cast(regionTexture, Texture));
indexData = mesh.getIndexData();
indices = QUAD_INDICES;
@ -148,7 +151,7 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
indexData = mesh.getIndexData();
attachmentColor = region.color;
uvs = region.uvs;
uvs = sequence.getUVs(sequenceIndex);
} else if (Std.isOfType(attachment, MeshAttachment)) {
var meshAttachment:MeshAttachment = cast(attachment, MeshAttachment);
verticesLength = meshAttachment.worldVerticesLength;
@ -157,13 +160,16 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
worldVertices.resize(verticesLength);
meshAttachment.computeWorldVertices(skeleton, slot, 0, meshAttachment.worldVerticesLength, worldVertices, 0, 2);
var sequence = meshAttachment.sequence;
var sequenceIndex = sequence.resolveIndex(pose);
mesh = null;
var regionTexture = sequence.regions[sequenceIndex].texture;
if (Std.isOfType(meshAttachment.rendererObject, SkeletonMesh)) {
mesh = cast(meshAttachment.rendererObject, SkeletonMesh);
mesh.texture = meshAttachment.region.texture;
mesh.texture = regionTexture;
indices = meshAttachment.triangles;
} else {
mesh = meshAttachment.rendererObject = new SkeletonMesh(cast(meshAttachment.region.texture, Texture));
mesh = meshAttachment.rendererObject = new SkeletonMesh(cast(regionTexture, Texture));
indexData = mesh.getIndexData();
indices = meshAttachment.triangles;
@ -177,7 +183,7 @@ class SkeletonSprite extends DisplayObject implements IAnimatable {
indexData = mesh.getIndexData();
attachmentColor = meshAttachment.color;
uvs = meshAttachment.uvs;
uvs = sequence.getUVs(sequenceIndex);
} else if (Std.isOfType(attachment, ClippingAttachment)) {
var clip:ClippingAttachment = cast(attachment, ClippingAttachment);
clipper.clipEnd(slot);