Sfoglia il codice sorgente

[ts] Port of sequence attachments, see #1956

SkeletonJson parsing of sequence timelines incomplete. Untested.
Mario Zechner 3 anni fa
parent
commit
81927051ff

+ 4 - 1
.gitignore

@@ -151,4 +151,7 @@ spine-ts/spine-core/dist
 spine-ts/spine-canvas/dist
 spine-ts/spine-webgl/dist
 spine-ts/spine-player/dist
-spine-ts/spine-threejs/dist
+spine-ts/spine-threejs/dist
+spine-libgdx/gradle
+spine-libgdx/gradlew
+spine-libgdx/gradlew.bat

+ 1 - 1
spine-ts/spine-canvas/src/SkeletonRenderer.ts

@@ -241,7 +241,7 @@ export class SkeletonRenderer {
 			skeletonColor.b * slotColor.b * regionColor.b * multiplier,
 			alpha);
 
-		region.computeWorldVertices(slot.bone, this.vertices, 0, SkeletonRenderer.VERTEX_SIZE);
+		region.computeWorldVertices(slot, this.vertices, 0, SkeletonRenderer.VERTEX_SIZE);
 
 		let vertices = this.vertices;
 		let uvs = region.uvs;

+ 95 - 3
spine-ts/spine-core/src/Animation.ts

@@ -35,6 +35,8 @@ import { Slot } from "./Slot";
 import { TransformConstraint } from "./TransformConstraint";
 import { StringSet, Utils, MathUtils, NumberArrayLike } from "./Utils";
 import { Event } from "./Event";
+import { HasTextureRegion } from "./attachments/HasTextureRegion";
+import { SequenceMode, SequenceModeValues } from "./attachments/Sequence";
 
 /** A simple container for a list of timelines and a name. */
 export class Animation {
@@ -146,7 +148,9 @@ const Property = {
 
 	pathConstraintPosition: 16,
 	pathConstraintSpacing: 17,
-	pathConstraintMix: 18
+	pathConstraintMix: 18,
+
+	sequence: 19
 }
 
 /** The interface for all timelines. */
@@ -1505,7 +1509,7 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline {
 		let slot: Slot = skeleton.slots[this.slotIndex];
 		if (!slot.bone.active) return;
 		let slotAttachment: Attachment = slot.getAttachment();
-		if (!(slotAttachment instanceof VertexAttachment) || (<VertexAttachment>slotAttachment).deformAttachment != this.attachment) return;
+		if (!(slotAttachment instanceof VertexAttachment) || (<VertexAttachment>slotAttachment).timelineAttahment != this.attachment) return;
 
 		let deform: Array<number> = slot.deform;
 		if (deform.length == 0) blend = MixBlend.setup;
@@ -1515,7 +1519,6 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline {
 
 		let frames = this.frames;
 		if (time < frames[0]) {
-			let vertexAttachment = <VertexAttachment>slotAttachment;
 			switch (blend) {
 				case MixBlend.setup:
 					deform.length = 0;
@@ -1526,6 +1529,7 @@ export class DeformTimeline extends CurveTimeline implements SlotTimeline {
 						return;
 					}
 					deform.length = vertexCount;
+					let vertexAttachment = <VertexAttachment>slotAttachment;
 					if (!vertexAttachment.bones) {
 						// Unweighted vertex positions.
 						let setupVertices = vertexAttachment.vertices;
@@ -2141,3 +2145,91 @@ export class PathConstraintMixTimeline extends CurveTimeline {
 		}
 	}
 }
+
+/** Changes a slot's {@link Slot#getSequenceIndex()} for an attachment's {@link Sequence}. */
+export class SequenceTimeline extends Timeline implements SlotTimeline {
+	static ENTRIES = 3;
+	static MODE = 1;
+	static DELAY = 2;
+
+	slotIndex: number;
+	attachment: HasTextureRegion;
+
+	constructor (frameCount: number, slotIndex: number, attachment: HasTextureRegion) {
+		super(frameCount, [
+			Property.sequence + "|" + slotIndex + "|" + attachment.sequence.id
+		]);
+		this.slotIndex = slotIndex;
+		this.attachment = attachment;
+	}
+
+	getFrameEntries () {
+		return SequenceTimeline.ENTRIES;
+	}
+
+	getSlotIndex () {
+		return this.slotIndex;
+	}
+
+	getAttachment () {
+		return this.attachment as unknown as Attachment;
+	}
+
+	/** Sets the time, mode, index, and frame time for the specified frame.
+	 * @param frame Between 0 and <code>frameCount</code>, inclusive.
+	 * @param time Seconds between frames. */
+	setFrame (frame: number, time: number, mode: SequenceMode, index: number, delay: number) {
+		let frames = this.frames;
+		frame *= SequenceTimeline.ENTRIES;
+		frames[frame] = time;
+		frames[frame + SequenceTimeline.MODE] = mode | (index << 4);
+		frames[frame + SequenceTimeline.DELAY] = delay;
+	}
+
+	apply (skeleton: Skeleton, lastTime: number, time: number, events: Array<Event>, alpha: number, blend: MixBlend, direction: MixDirection) {
+		let slot = skeleton.slots[this.slotIndex];
+		if (!slot.bone.active) return;
+		let slotAttachment = slot.attachment;
+		let attachment = this.attachment as unknown as Attachment;
+		if (slotAttachment != attachment) {
+			if (!(slotAttachment instanceof VertexAttachment)
+				|| (slotAttachment as VertexAttachment).timelineAttahment != attachment) return;
+		}
+
+		let frames = this.frames;
+		if (time < frames[0]) { // Time is before first frame.
+			if (blend == MixBlend.setup || blend == MixBlend.first) slot.sequenceIndex = -1;
+			return;
+		}
+
+		let i = Timeline.search(frames, time, SequenceTimeline.ENTRIES);
+		let before = frames[i];
+		let modeAndIndex = frames[i + SequenceTimeline.MODE];
+		let delay = frames[i + SequenceTimeline.DELAY];
+
+		let index = modeAndIndex >> 4, count = this.attachment.sequence.regions.length;
+		let mode = SequenceModeValues[modeAndIndex & 0xf];
+		if (mode != SequenceMode.hold) {
+			index += (time - before) / delay + 0.00001;
+			switch (mode) {
+				case SequenceMode.once:
+					index = Math.min(count - 1, index);
+					break;
+				case SequenceMode.loop:
+					index %= count;
+					break;
+				case SequenceMode.pingpong:
+					let n = (count << 1) - 2;
+					index %= n;
+					if (index >= count) index = n - index;
+					break;
+				case SequenceMode.onceReverse:
+					index = Math.max(count - 1 - index, 0);
+					break;
+				case SequenceMode.loopReverse:
+					index = count - 1 - (index % count);
+			}
+		}
+		slot.sequenceIndex = index;
+	}
+}

+ 29 - 10
spine-ts/spine-core/src/AtlasAttachmentLoader.ts

@@ -36,6 +36,7 @@ import { PointAttachment } from "./attachments/PointAttachment";
 import { RegionAttachment } from "./attachments/RegionAttachment";
 import { Skin } from "./Skin";
 import { TextureAtlas } from "./TextureAtlas";
+import { Sequence } from "./attachments/Sequence"
 
 /** An {@link AttachmentLoader} that configures attachments using texture regions from an {@link TextureAtlas}.
  *
@@ -48,21 +49,39 @@ export class AtlasAttachmentLoader implements AttachmentLoader {
 		this.atlas = atlas;
 	}
 
-	newRegionAttachment (skin: Skin, name: string, path: string): RegionAttachment {
-		let region = this.atlas.findRegion(path);
-		if (!region) throw new Error("Region not found in atlas: " + path + " (region attachment: " + name + ")");
-		region.renderObject = region;
+	loadSequence (name: string, basePath: string, sequence: Sequence) {
+		let regions = sequence.regions;
+		for (let i = 0, n = regions.length; i < n; i++) {
+			let path = sequence.getPath(basePath, i);
+			regions[i] = this.atlas.findRegion(path);
+			regions[i].renderObject = regions[i];
+			if (regions[i] == null) throw new Error("Region not found in atlas: " + path + " (sequence: " + name + ")");
+		}
+	}
+
+	newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence): RegionAttachment {
 		let attachment = new RegionAttachment(name);
-		attachment.setRegion(region);
+		if (sequence != null) {
+			this.loadSequence(name, path, sequence);
+		} else {
+			let region = this.atlas.findRegion(path);
+			if (!region) throw new Error("Region not found in atlas: " + path + " (region attachment: " + name + ")");
+			region.renderObject = region;
+			attachment.region = region;
+		}
 		return attachment;
 	}
 
-	newMeshAttachment (skin: Skin, name: string, path: string): MeshAttachment {
-		let region = this.atlas.findRegion(path);
-		if (!region) throw new Error("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
-		region.renderObject = region;
+	newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment {
 		let attachment = new MeshAttachment(name);
-		attachment.region = region;
+		if (sequence != null) {
+			this.loadSequence(name, path, sequence);
+		} else {
+			let region = this.atlas.findRegion(path);
+			if (!region) throw new Error("Region not found in atlas: " + path + " (mesh attachment: " + name + ")");
+			region.renderObject = region;
+			attachment.region = region;
+		}
 		return attachment;
 	}
 

+ 1 - 11
spine-ts/spine-core/src/Skeleton.ts

@@ -75,11 +75,6 @@ export class Skeleton {
 	/** The color to tint all the skeleton's attachments. */
 	color: Color;
 
-	/** Returns the skeleton's time. This can be used for tracking, such as with Slot {@link Slot#attachmentTime}.
-	 * <p>
-	 * See {@link #update()}. */
-	time = 0;
-
 	/** Scales the entire skeleton on the X axis. This affects all bones, even if the bone's transform mode disallows scale
 	  * inheritance. */
 	scaleX = 1;
@@ -603,7 +598,7 @@ export class Skeleton {
 			if (attachment instanceof RegionAttachment) {
 				verticesLength = 8;
 				vertices = Utils.setArraySize(temp, verticesLength, 0);
-				(<RegionAttachment>attachment).computeWorldVertices(slot.bone, vertices, 0, 2);
+				(<RegionAttachment>attachment).computeWorldVertices(slot, vertices, 0, 2);
 			} else if (attachment instanceof MeshAttachment) {
 				let mesh = (<MeshAttachment>attachment);
 				verticesLength = mesh.worldVerticesLength;
@@ -623,9 +618,4 @@ export class Skeleton {
 		offset.set(minX, minY);
 		size.set(maxX - minX, maxY - minY);
 	}
-
-	/** Increments the skeleton's {@link #time}. */
-	update (delta: number) {
-		this.time += delta;
-	}
 }

+ 139 - 102
spine-ts/spine-core/src/SkeletonBinary.ts

@@ -27,10 +27,12 @@
  * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
  *****************************************************************************/
 
-import { Animation, Timeline, AttachmentTimeline, RGBATimeline, RGBTimeline, RGBA2Timeline, RGB2Timeline, AlphaTimeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline2, CurveTimeline } from "./Animation";
+import { Animation, Timeline, AttachmentTimeline, RGBATimeline, RGBTimeline, RGBA2Timeline, RGB2Timeline, AlphaTimeline, RotateTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, IkConstraintTimeline, TransformConstraintTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PathConstraintMixTimeline, DeformTimeline, DrawOrderTimeline, EventTimeline, CurveTimeline1, CurveTimeline2, CurveTimeline, SequenceTimeline } from "./Animation";
 import { VertexAttachment, Attachment } from "./attachments/Attachment";
 import { AttachmentLoader } from "./attachments/AttachmentLoader";
+import { HasTextureRegion } from "./attachments/HasTextureRegion";
 import { MeshAttachment } from "./attachments/MeshAttachment";
+import { Sequence, SequenceModeValues } from "./attachments/Sequence";
 import { BoneData } from "./BoneData";
 import { Event } from "./Event";
 import { EventData } from "./EventData";
@@ -219,9 +221,9 @@ export class SkeletonBinary {
 			let linkedMesh = this.linkedMeshes[i];
 			let skin = !linkedMesh.skin ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin);
 			let parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
-			linkedMesh.mesh.deformAttachment = linkedMesh.inheritDeform ? parent as VertexAttachment : linkedMesh.mesh;
+			linkedMesh.mesh.timelineAttahment = linkedMesh.inheritTimeline ? parent as VertexAttachment : linkedMesh.mesh;
 			linkedMesh.mesh.setParentMesh(parent as MeshAttachment);
-			linkedMesh.mesh.updateUVs();
+			if (linkedMesh.mesh.region != null) linkedMesh.mesh.updateRegion();
 		}
 		this.linkedMeshes.length = 0;
 
@@ -299,9 +301,10 @@ export class SkeletonBinary {
 				let width = input.readFloat();
 				let height = input.readFloat();
 				let color = input.readInt32();
+				let sequence = this.readSequence(input);
 
 				if (!path) path = name;
-				let region = this.attachmentLoader.newRegionAttachment(skin, name, path);
+				let region = this.attachmentLoader.newRegionAttachment(skin, name, path, sequence);
 				if (!region) return null;
 				region.path = path;
 				region.x = x * scale;
@@ -312,7 +315,8 @@ export class SkeletonBinary {
 				region.width = width * scale;
 				region.height = height * scale;
 				Color.rgba8888ToColor(region.color, color);
-				region.updateOffset();
+				region.sequence = sequence;
+				if (sequence == null) region.updateRegion();
 				return region;
 			}
 			case AttachmentType.BoundingBox: {
@@ -336,6 +340,7 @@ export class SkeletonBinary {
 				let triangles = this.readShortArray(input);
 				let vertices = this.readVertices(input, vertexCount);
 				let hullLength = input.readInt(true);
+				let sequence = this.readSequence(input);
 				let edges = null;
 				let width = 0, height = 0;
 				if (nonessential) {
@@ -345,7 +350,7 @@ export class SkeletonBinary {
 				}
 
 				if (!path) path = name;
-				let mesh = this.attachmentLoader.newMeshAttachment(skin, name, path);
+				let mesh = this.attachmentLoader.newMeshAttachment(skin, name, path, sequence);
 				if (!mesh) return null;
 				mesh.path = path;
 				Color.rgba8888ToColor(mesh.color, color);
@@ -354,8 +359,9 @@ export class SkeletonBinary {
 				mesh.worldVerticesLength = vertexCount << 1;
 				mesh.triangles = triangles;
 				mesh.regionUVs = uvs;
-				mesh.updateUVs();
+				if (sequence == null) mesh.updateRegion();
 				mesh.hullLength = hullLength << 1;
+				mesh.sequence = sequence;
 				if (nonessential) {
 					mesh.edges = edges;
 					mesh.width = width * scale;
@@ -368,7 +374,8 @@ export class SkeletonBinary {
 				let color = input.readInt32();
 				let skinName = input.readStringRef();
 				let parent = input.readStringRef();
-				let inheritDeform = input.readBoolean();
+				let inheritTimelines = input.readBoolean();
+				let sequence = this.readSequence(input);
 				let width = 0, height = 0;
 				if (nonessential) {
 					width = input.readFloat();
@@ -376,15 +383,16 @@ export class SkeletonBinary {
 				}
 
 				if (!path) path = name;
-				let mesh = this.attachmentLoader.newMeshAttachment(skin, name, path);
+				let mesh = this.attachmentLoader.newMeshAttachment(skin, name, path, sequence);
 				if (!mesh) return null;
 				mesh.path = path;
 				Color.rgba8888ToColor(mesh.color, color);
+				mesh.sequence = sequence;
 				if (nonessential) {
 					mesh.width = width * scale;
 					mesh.height = height * scale;
 				}
-				this.linkedMeshes.push(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritDeform));
+				this.linkedMeshes.push(new LinkedMesh(mesh, skinName, slotIndex, parent, inheritTimelines));
 				return mesh;
 			}
 			case AttachmentType.Path: {
@@ -441,6 +449,15 @@ export class SkeletonBinary {
 		return null;
 	}
 
+	private readSequence (input: BinaryInput) {
+		if (!input.readBoolean()) return null;
+		let sequence = new Sequence(input.readInt(true));
+		sequence.start = input.readInt(true);
+		sequence.digits = input.readInt(true);
+		sequence.setupIndex = input.readInt(true);
+		return sequence;
+	}
+
 	private readVertices (input: BinaryInput, vertexCount: number): Vertices {
 		let scale = this.scale;
 		let verticesLength = vertexCount << 1;
@@ -697,7 +714,6 @@ export class SkeletonBinary {
 							a = a2;
 						}
 						timelines.push(timeline);
-						break;
 					}
 				}
 			}
@@ -850,112 +866,130 @@ export class SkeletonBinary {
 				let slotIndex = input.readInt(true);
 				for (let iii = 0, nnn = input.readInt(true); iii < nnn; iii++) {
 					let attachmentName = input.readStringRef();
-					let attachment = skin.getAttachment(slotIndex, attachmentName) as VertexAttachment;
-					let weighted = attachment.bones;
-					let vertices = attachment.vertices;
-					let deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
-
+					let attachment = skin.getAttachment(slotIndex, attachmentName);
+					let timelineType = input.readByte();
 					let frameCount = input.readInt(true);
 					let frameLast = frameCount - 1;
-					let bezierCount = input.readInt(true);
-					let timeline = new DeformTimeline(frameCount, bezierCount, slotIndex, attachment);
 
-					let time = input.readFloat();
-					for (let frame = 0, bezier = 0; ; frame++) {
-						let deform;
-						let end = input.readInt(true);
-						if (end == 0)
-							deform = weighted ? Utils.newFloatArray(deformLength) : vertices;
-						else {
-							deform = Utils.newFloatArray(deformLength);
-							let start = input.readInt(true);
-							end += start;
-							if (scale == 1) {
-								for (let v = start; v < end; v++)
-									deform[v] = input.readFloat();
-							} else {
-								for (let v = start; v < end; v++)
-									deform[v] = input.readFloat() * scale;
-							}
-							if (!weighted) {
-								for (let v = 0, vn = deform.length; v < vn; v++)
-									deform[v] += vertices[v];
+					switch (timelineType) {
+						case ATTACHMENT_DEFORM: {
+							let vertexAttachment = attachment as VertexAttachment;
+							let weighted = vertexAttachment.bones;
+							let vertices = vertexAttachment.vertices;
+							let deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
+
+
+							let bezierCount = input.readInt(true);
+							let timeline = new DeformTimeline(frameCount, bezierCount, slotIndex, vertexAttachment);
+
+							let time = input.readFloat();
+							for (let frame = 0, bezier = 0; ; frame++) {
+								let deform;
+								let end = input.readInt(true);
+								if (end == 0)
+									deform = weighted ? Utils.newFloatArray(deformLength) : vertices;
+								else {
+									deform = Utils.newFloatArray(deformLength);
+									let start = input.readInt(true);
+									end += start;
+									if (scale == 1) {
+										for (let v = start; v < end; v++)
+											deform[v] = input.readFloat();
+									} else {
+										for (let v = start; v < end; v++)
+											deform[v] = input.readFloat() * scale;
+									}
+									if (!weighted) {
+										for (let v = 0, vn = deform.length; v < vn; v++)
+											deform[v] += vertices[v];
+									}
+								}
+
+								timeline.setFrame(frame, time, deform);
+								if (frame == frameLast) break;
+								let time2 = input.readFloat();
+								switch (input.readByte()) {
+									case CURVE_STEPPED:
+										timeline.setStepped(frame);
+										break;
+									case CURVE_BEZIER:
+										setBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
+								}
+								time = time2;
 							}
+							timelines.push(timeline);
 						}
-
-						timeline.setFrame(frame, time, deform);
-						if (frame == frameLast) break;
-						let time2 = input.readFloat();
-						switch (input.readByte()) {
-							case CURVE_STEPPED:
-								timeline.setStepped(frame);
-								break;
-							case CURVE_BEZIER:
-								setBezier(input, timeline, bezier++, frame, 0, time, time2, 0, 1, 1);
+						case ATTACHMENT_SEQUENCE: {
+							let timeline = new SequenceTimeline(frameCount, slotIndex, attachment as unknown as HasTextureRegion);
+							for (let frame = 0; frame < frameCount; frame++) {
+								let time = input.readFloat();
+								let modeAndIndex = input.readInt32();
+								timeline.setFrame(frame, time, SequenceModeValues[modeAndIndex & 0xf], modeAndIndex >> 4,
+									input.readFloat());
+							}
+							timelines.push(timeline);
 						}
-						time = time2;
 					}
-					timelines.push(timeline);
 				}
 			}
-		}
 
-		// Draw order timeline.
-		let drawOrderCount = input.readInt(true);
-		if (drawOrderCount > 0) {
-			let timeline = new DrawOrderTimeline(drawOrderCount);
-			let slotCount = skeletonData.slots.length;
-			for (let i = 0; i < drawOrderCount; i++) {
-				let time = input.readFloat();
-				let offsetCount = input.readInt(true);
-				let drawOrder = Utils.newArray(slotCount, 0);
-				for (let ii = slotCount - 1; ii >= 0; ii--)
-					drawOrder[ii] = -1;
-				let unchanged = Utils.newArray(slotCount - offsetCount, 0);
-				let originalIndex = 0, unchangedIndex = 0;
-				for (let ii = 0; ii < offsetCount; ii++) {
-					let slotIndex = input.readInt(true);
-					// Collect unchanged items.
-					while (originalIndex != slotIndex)
+			// Draw order timeline.
+			let drawOrderCount = input.readInt(true);
+			if (drawOrderCount > 0) {
+				let timeline = new DrawOrderTimeline(drawOrderCount);
+				let slotCount = skeletonData.slots.length;
+				for (let i = 0; i < drawOrderCount; i++) {
+					let time = input.readFloat();
+					let offsetCount = input.readInt(true);
+					let drawOrder = Utils.newArray(slotCount, 0);
+					for (let ii = slotCount - 1; ii >= 0; ii--)
+						drawOrder[ii] = -1;
+					let unchanged = Utils.newArray(slotCount - offsetCount, 0);
+					let originalIndex = 0, unchangedIndex = 0;
+					for (let ii = 0; ii < offsetCount; ii++) {
+						let 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++;
-					// Set changed items.
-					drawOrder[originalIndex + input.readInt(true)] = originalIndex++;
+					// Fill in unchanged items.
+					for (let ii = slotCount - 1; ii >= 0; ii--)
+						if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex];
+					timeline.setFrame(i, time, drawOrder);
 				}
-				// Collect remaining unchanged items.
-				while (originalIndex < slotCount)
-					unchanged[unchangedIndex++] = originalIndex++;
-				// Fill in unchanged items.
-				for (let ii = slotCount - 1; ii >= 0; ii--)
-					if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex];
-				timeline.setFrame(i, time, drawOrder);
+				timelines.push(timeline);
 			}
-			timelines.push(timeline);
-		}
 
-		// Event timeline.
-		let eventCount = input.readInt(true);
-		if (eventCount > 0) {
-			let timeline = new EventTimeline(eventCount);
-			for (let i = 0; i < eventCount; i++) {
-				let time = input.readFloat();
-				let eventData = skeletonData.events[input.readInt(true)];
-				let event = new Event(time, eventData);
-				event.intValue = input.readInt(false);
-				event.floatValue = input.readFloat();
-				event.stringValue = input.readBoolean() ? input.readString() : eventData.stringValue;
-				if (event.data.audioPath) {
-					event.volume = input.readFloat();
-					event.balance = input.readFloat();
+			// Event timeline.
+			let eventCount = input.readInt(true);
+			if (eventCount > 0) {
+				let timeline = new EventTimeline(eventCount);
+				for (let i = 0; i < eventCount; i++) {
+					let time = input.readFloat();
+					let eventData = skeletonData.events[input.readInt(true)];
+					let event = new Event(time, eventData);
+					event.intValue = input.readInt(false);
+					event.floatValue = input.readFloat();
+					event.stringValue = input.readBoolean() ? input.readString() : eventData.stringValue;
+					if (event.data.audioPath) {
+						event.volume = input.readFloat();
+						event.balance = input.readFloat();
+					}
+					timeline.setFrame(i, event);
 				}
-				timeline.setFrame(i, event);
+				timelines.push(timeline);
 			}
-			timelines.push(timeline);
-		}
 
-		let duration = 0;
-		for (let i = 0, n = timelines.length; i < n; i++)
-			duration = Math.max(duration, timelines[i].getDuration());
-		return new Animation(name, timelines, duration);
+			let duration = 0;
+			for (let i = 0, n = timelines.length; i < n; i++)
+				duration = Math.max(duration, timelines[i].getDuration());
+			return new Animation(name, timelines, duration);
+		}
 	}
 }
 
@@ -1056,14 +1090,14 @@ class LinkedMesh {
 	parent: string; skin: string;
 	slotIndex: number;
 	mesh: MeshAttachment;
-	inheritDeform: boolean;
+	inheritTimeline: boolean;
 
 	constructor (mesh: MeshAttachment, skin: string, slotIndex: number, parent: string, inheritDeform: boolean) {
 		this.mesh = mesh;
 		this.skin = skin;
 		this.slotIndex = slotIndex;
 		this.parent = parent;
-		this.inheritDeform = inheritDeform;
+		this.inheritTimeline = inheritDeform;
 	}
 }
 
@@ -1136,6 +1170,9 @@ const SLOT_RGBA2 = 3;
 const SLOT_RGB2 = 4;
 const SLOT_ALPHA = 5;
 
+const ATTACHMENT_DEFORM = 0;
+const ATTACHMENT_SEQUENCE = 1;
+
 const PATH_POSITION = 0;
 const PATH_SPACING = 1;
 const PATH_MIX = 2;

+ 36 - 21
spine-ts/spine-core/src/SkeletonJson.ts

@@ -41,6 +41,7 @@ import { Skin } from "./Skin";
 import { SlotData, BlendMode } from "./SlotData";
 import { TransformConstraintData } from "./TransformConstraintData";
 import { Utils, Color, NumberArrayLike } from "./Utils";
+import { Sequence } from "./attachments/Sequence";
 
 /** Loads skeleton data in the Spine JSON format.
  *
@@ -257,9 +258,9 @@ export class SkeletonJson {
 			let linkedMesh = this.linkedMeshes[i];
 			let skin = !linkedMesh.skin ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin);
 			let parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent);
-			linkedMesh.mesh.deformAttachment = linkedMesh.inheritDeform ? <VertexAttachment>parent : <VertexAttachment>linkedMesh.mesh;
+			linkedMesh.mesh.timelineAttahment = linkedMesh.inheritTimeline ? <VertexAttachment>parent : <VertexAttachment>linkedMesh.mesh;
 			linkedMesh.mesh.setParentMesh(<MeshAttachment>parent);
-			linkedMesh.mesh.updateUVs();
+			if (linkedMesh.mesh.region != null) linkedMesh.mesh.updateRegion();
 		}
 		this.linkedMeshes.length = 0;
 
@@ -298,7 +299,8 @@ export class SkeletonJson {
 		switch (getValue(map, "type", "region")) {
 			case "region": {
 				let path = getValue(map, "path", name);
-				let region = this.attachmentLoader.newRegionAttachment(skin, name, path);
+				let sequence = this.readSequence(getValue(map, "sequence", null));
+				let region = this.attachmentLoader.newRegionAttachment(skin, name, path, sequence);
 				if (!region) return null;
 				region.path = path;
 				region.x = getValue(map, "x", 0) * scale;
@@ -308,11 +310,12 @@ export class SkeletonJson {
 				region.rotation = getValue(map, "rotation", 0);
 				region.width = map.width * scale;
 				region.height = map.height * scale;
+				region.sequence = sequence;
 
 				let color: string = getValue(map, "color", null);
 				if (color) region.color.setFromString(color);
 
-				region.updateOffset();
+				if (region.region != null) region.updateRegion();
 				return region;
 			}
 			case "boundingbox": {
@@ -326,7 +329,8 @@ export class SkeletonJson {
 			case "mesh":
 			case "linkedmesh": {
 				let path = getValue(map, "path", name);
-				let mesh = this.attachmentLoader.newMeshAttachment(skin, name, path);
+				let sequence = this.readSequence(getValue(map, "sequence", null));
+				let mesh = this.attachmentLoader.newMeshAttachment(skin, name, path, sequence);
 				if (!mesh) return null;
 				mesh.path = path;
 
@@ -335,10 +339,11 @@ export class SkeletonJson {
 
 				mesh.width = getValue(map, "width", 0) * scale;
 				mesh.height = getValue(map, "height", 0) * scale;
+				mesh.sequence = sequence;
 
 				let parent: string = getValue(map, "parent", null);
 				if (parent) {
-					this.linkedMeshes.push(new LinkedMesh(mesh, <string>getValue(map, "skin", null), slotIndex, parent, getValue(map, "deform", true)));
+					this.linkedMeshes.push(new LinkedMesh(mesh, <string>getValue(map, "skin", null), slotIndex, parent, getValue(map, "timelines", true)));
 					return mesh;
 				}
 
@@ -346,7 +351,7 @@ export class SkeletonJson {
 				this.readVertices(map, mesh, uvs.length);
 				mesh.triangles = map.triangles;
 				mesh.regionUVs = uvs;
-				mesh.updateUVs();
+				if (mesh.region != null) mesh.updateRegion();
 
 				mesh.edges = getValue(map, "edges", null);
 				mesh.hullLength = getValue(map, "hull", 0) * 2;
@@ -399,6 +404,15 @@ export class SkeletonJson {
 		return null;
 	}
 
+	readSequence (map: any) {
+		if (map == null) return null;
+		let sequence = new Sequence(getValue(map, "count", 0));
+		sequence.start = getValue(map, "start", 1);
+		sequence.digits = getValue(map, "digits", 0);
+		sequence.setupIndex = getValue(map, "setup", 0);
+		return sequence;
+	}
+
 	readVertices (map: any, attachment: VertexAttachment, verticesLength: number) {
 		let scale = this.scale;
 		attachment.worldVerticesLength = verticesLength;
@@ -445,7 +459,7 @@ export class SkeletonJson {
 						let timeline = new AttachmentTimeline(frames, slotIndex);
 						for (let frame = 0; frame < frames; frame++) {
 							let keyMap = timelineMap[frame];
-							timeline.setFrame(frame, getValue(keyMap, "time", 0), keyMap.name);
+							timeline.setFrame(frame, getValue(keyMap, "time", 0), getValue(keyMap, "name", null));
 						}
 						timelines.push(timeline);
 
@@ -778,17 +792,18 @@ export class SkeletonJson {
 			}
 		}
 
-		// Deform timelines.
-		if (map.deform) {
-			for (let deformName in map.deform) {
-				let deformMap = map.deform[deformName];
-				let skin = skeletonData.findSkin(deformName);
-				for (let slotName in deformMap) {
-					let slotMap = deformMap[slotName];
+		// Attachment timelines.
+		if (map.attachments) {
+			for (let attachmentsName in map.attachments) {
+				let attachmentsMap = map.attachments[attachmentsName];
+				let skin = skeletonData.findSkin(attachmentsName);
+				for (let slotName in attachmentsMap) {
+					let slotMap = attachmentsMap[slotName];
 					let slotIndex = skeletonData.findSlot(slotName).index;
 					for (let timelineName in slotMap) {
-						let timelineMap = slotMap[timelineName];
-						let keyMap = timelineMap[0];
+						let attachmentMap = slotMap[timelineName];
+						let attachmentMapName = timelineName;
+						let keyMap = attachmentMap[0];
 						if (!keyMap) continue;
 
 						let attachment = <VertexAttachment>skin.getAttachment(slotIndex, timelineName);
@@ -796,7 +811,7 @@ export class SkeletonJson {
 						let vertices = attachment.vertices;
 						let deformLength = weighted ? vertices.length / 3 * 2 : vertices.length;
 
-						let timeline = new DeformTimeline(timelineMap.length, timelineMap.length, slotIndex, attachment);
+						let timeline = new DeformTimeline(attachmentMap.length, attachmentMap.length, slotIndex, attachment);
 						let time = getValue(keyMap, "time", 0);
 						for (let frame = 0, bezier = 0; ; frame++) {
 							let deform: NumberArrayLike;
@@ -818,7 +833,7 @@ export class SkeletonJson {
 							}
 
 							timeline.setFrame(frame, time, deform);
-							let nextMap = timelineMap[frame + 1];
+							let nextMap = attachmentMap[frame + 1];
 							if (!nextMap) {
 								timeline.shrink(bezier);
 								break;
@@ -900,14 +915,14 @@ class LinkedMesh {
 	parent: string; skin: string;
 	slotIndex: number;
 	mesh: MeshAttachment;
-	inheritDeform: boolean;
+	inheritTimeline: boolean;
 
 	constructor (mesh: MeshAttachment, skin: string, slotIndex: number, parent: string, inheritDeform: boolean) {
 		this.mesh = mesh;
 		this.skin = skin;
 		this.slotIndex = slotIndex;
 		this.parent = parent;
-		this.inheritDeform = inheritDeform;
+		this.inheritTimeline = inheritDeform;
 	}
 }
 

+ 9 - 18
spine-ts/spine-core/src/Slot.ts

@@ -53,10 +53,12 @@ export class Slot {
 
 	attachment: Attachment;
 
-	private attachmentTime: number;
-
 	attachmentState: number;
 
+	/** The index of the texture region to display when the slot's attachment has a {@link Sequence}. -1 represents the
+	 * {@link Sequence#getSetupIndex()}. */
+	sequenceIndex: number;
+
 	/** Values to deform the slot's attachment. For an unweighted mesh, the entries are local positions for each vertex. For a
 	 * weighted mesh, the entries are an offset for each vertex which will be added to the mesh's local vertex positions.
 	 *
@@ -83,28 +85,17 @@ export class Slot {
 		return this.attachment;
 	}
 
-	/** Sets the slot's attachment and, if the attachment changed, resets {@link #attachmentTime} and clears the {@link #deform}.
-	 * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getDeformAttachment()} as the specified
-	 * attachment.
-	 * @param attachment May be null. */
+	/** Sets the slot's attachment and, if the attachment changed, resets {@link #sequenceIndex} and clears the {@link #deform}.
+	 * The deform is not cleared if the old attachment has the same {@link VertexAttachment#getTimelineAttachment()} as the
+	 * specified attachment. */
 	setAttachment (attachment: Attachment) {
 		if (this.attachment == attachment) return;
 		if (!(attachment instanceof VertexAttachment) || !(this.attachment instanceof VertexAttachment)
-			|| (<VertexAttachment>attachment).deformAttachment != (<VertexAttachment>this.attachment).deformAttachment) {
+			|| (<VertexAttachment>attachment).timelineAttahment != (<VertexAttachment>this.attachment).timelineAttahment) {
 			this.deform.length = 0;
 		}
 		this.attachment = attachment;
-		this.attachmentTime = this.bone.skeleton.time;
-	}
-
-	setAttachmentTime (time: number) {
-		this.attachmentTime = this.bone.skeleton.time - time;
-	}
-
-	/** The time that has elapsed since the last time the attachment was set or cleared. Relies on Skeleton
-	 * {@link Skeleton#time}. */
-	getAttachmentTime (): number {
-		return this.bone.skeleton.time - this.attachmentTime;
+		this.sequenceIndex = -1;
 	}
 
 	/** Sets this slot to the setup pose. */

+ 4 - 3
spine-ts/spine-core/src/attachments/Attachment.ts

@@ -64,8 +64,9 @@ export abstract class VertexAttachment extends Attachment {
 	 * {@link #computeWorldVertices()} using the `count` parameter. */
 	worldVerticesLength = 0;
 
-	/** Deform keys for the deform attachment are also applied to this attachment. May be null if no deform keys should be applied. */
-	deformAttachment: VertexAttachment = this;
+	/** Timelines for the timeline attachment are also applied to this attachment.
+	 * May be null if no attachment-specific timelines should be applied. */
+	timelineAttahment: Attachment = this;
 
 	constructor (name: string) {
 		super(name);
@@ -155,6 +156,6 @@ export abstract class VertexAttachment extends Attachment {
 			attachment.vertices = null;
 
 		attachment.worldVerticesLength = this.worldVerticesLength;
-		attachment.deformAttachment = this.deformAttachment;
+		attachment.timelineAttahment = this.timelineAttahment;
 	}
 }

+ 3 - 2
spine-ts/spine-core/src/attachments/AttachmentLoader.ts

@@ -34,6 +34,7 @@ import { MeshAttachment } from "./MeshAttachment";
 import { PathAttachment } from "./PathAttachment";
 import { PointAttachment } from "./PointAttachment";
 import { RegionAttachment } from "./RegionAttachment";
+import { Sequence } from "./Sequence";
 
 /** The interface which can be implemented to customize creating and populating attachments.
  *
@@ -41,10 +42,10 @@ import { RegionAttachment } from "./RegionAttachment";
  * Runtimes Guide. */
 export interface AttachmentLoader {
 	/** @return May be null to not load an attachment. */
-	newRegionAttachment (skin: Skin, name: string, path: string): RegionAttachment;
+	newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence): RegionAttachment;
 
 	/** @return May be null to not load an attachment. */
-	newMeshAttachment (skin: Skin, name: string, path: string): MeshAttachment;
+	newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment;
 
 	/** @return May be null to not load an attachment. */
 	newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment;

+ 50 - 0
spine-ts/spine-core/src/attachments/HasTextureRegion.ts

@@ -0,0 +1,50 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated September 24, 2021. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2021, 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.
+ *****************************************************************************/
+
+import { TextureRegion } from "../Texture"
+import { Color } from "../Utils"
+import { Sequence } from "./Sequence"
+
+export interface HasTextureRegion {
+	/** The name used to find the {@link #region()}. */
+	path: string;
+
+	/** The region used to draw the attachment. After setting the region or if the region's properties are changed,
+	 * {@link #updateRegion()} must be called. */
+	region: TextureRegion;
+
+	/** Updates any values the attachment calculates using the {@link #getRegion()}. Must be called after setting the
+	 * {@link #getRegion()} or if the region's properties are changed. */
+	updateRegion (): void;
+
+	/** The color to tint the attachment. */
+	color: Color;
+
+	sequence: Sequence;
+}

+ 19 - 6
spine-ts/spine-core/src/attachments/MeshAttachment.ts

@@ -31,12 +31,15 @@ import { TextureRegion } from "../Texture";
 import { TextureAtlasRegion } from "../TextureAtlas";
 import { Color, NumberArrayLike, Utils } from "../Utils";
 import { VertexAttachment, Attachment } from "./Attachment";
+import { HasTextureRegion } from "./HasTextureRegion";
+import { Sequence } from "./Sequence";
+import { Slot } from "../Slot";
 
 /** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
  * supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
  *
  * See [Mesh attachments](http://esotericsoftware.com/spine-meshes) in the Spine User Guide. */
-export class MeshAttachment extends VertexAttachment {
+export class MeshAttachment extends VertexAttachment implements HasTextureRegion {
 	region: TextureRegion;
 
 	/** The name of the texture region for this attachment. */
@@ -70,15 +73,18 @@ export class MeshAttachment extends VertexAttachment {
 	edges: Array<number>;
 
 	private parentMesh: MeshAttachment;
+
+	sequence: Sequence;
+
 	tempColor = new Color(0, 0, 0, 0);
 
 	constructor (name: string) {
 		super(name);
 	}
 
-	/** Calculates {@link #uvs} using {@link #regionUVs} and the {@link #region}. Must be called after changing the region UVs or
-	 * region. */
-	updateUVs () {
+	/** Calculates {@link #uvs} using the {@link #regionUVs} and region. Must be called if the region, the region's properties, or
+	 * the {@link #regionUVs} are changed. */
+	updateRegion () {
 		let regionUVs = this.regionUVs;
 		if (!this.uvs || this.uvs.length != regionUVs.length) this.uvs = Utils.newFloatArray(regionUVs.length);
 		let uvs = this.uvs;
@@ -175,6 +181,8 @@ export class MeshAttachment extends VertexAttachment {
 		Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length);
 		copy.hullLength = this.hullLength;
 
+		copy.sequence = this.sequence.copy();
+
 		// Nonessential.
 		if (this.edges) {
 			copy.edges = new Array<number>(this.edges.length);
@@ -186,15 +194,20 @@ export class MeshAttachment extends VertexAttachment {
 		return copy;
 	}
 
+	computeWorldVertices (slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) {
+		if (this.sequence != null) this.sequence.apply(slot, this);
+		super.computeWorldVertices(slot, start, count, worldVertices, offset, stride);
+	}
+
 	/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/
 	newLinkedMesh (): MeshAttachment {
 		let copy = new MeshAttachment(this.name);
 		copy.region = this.region;
 		copy.path = this.path;
 		copy.color.setFromColor(this.color);
-		copy.deformAttachment = this.deformAttachment;
+		copy.timelineAttahment = this.timelineAttahment;
 		copy.setParentMesh(this.parentMesh ? this.parentMesh : this);
-		copy.updateUVs();
+		if (copy.region != null) copy.updateRegion();
 		return copy;
 	}
 }

+ 19 - 13
spine-ts/spine-core/src/attachments/RegionAttachment.ts

@@ -31,11 +31,14 @@ import { Bone } from "../Bone";
 import { TextureRegion } from "../Texture";
 import { Color, NumberArrayLike, Utils } from "../Utils";
 import { Attachment } from "./Attachment";
+import { HasTextureRegion } from "./HasTextureRegion";
+import { Sequence } from "./Sequence";
+import { Slot } from "../Slot";
 
 /** An attachment that displays a textured quadrilateral.
  *
  * See [Region attachments](http://esotericsoftware.com/spine-regions) in the Spine User Guide. */
-export class RegionAttachment extends Attachment {
+export class RegionAttachment extends Attachment implements HasTextureRegion {
 	/** The local x translation. */
 	x = 0;
 
@@ -63,8 +66,9 @@ export class RegionAttachment extends Attachment {
 	/** The name of the texture region for this attachment. */
 	path: string;
 
-	rendererObject: any;
+	private rendererObject: any;
 	region: TextureRegion;
+	sequence: Sequence;
 
 	/** For each of the 4 vertices, a pair of <code>x,y</code> values that is the local position of the vertex.
 	 *
@@ -80,7 +84,7 @@ export class RegionAttachment extends Attachment {
 	}
 
 	/** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */
-	updateOffset (): void {
+	updateRegion (): void {
 		let region = this.region;
 		let regionScaleX = this.width / this.region.originalWidth * this.scaleX;
 		let regionScaleY = this.height / this.region.originalHeight * this.scaleY;
@@ -109,10 +113,7 @@ export class RegionAttachment extends Attachment {
 		offset[5] = localY2Cos + localX2Sin;
 		offset[6] = localX2Cos - localYSin;
 		offset[7] = localYCos + localX2Sin;
-	}
 
-	setRegion (region: TextureRegion): void {
-		this.region = region;
 		let uvs = this.uvs;
 		if (region.degrees == 90) {
 			uvs[2] = region.u;
@@ -135,14 +136,18 @@ export class RegionAttachment extends Attachment {
 		}
 	}
 
-	/** Transforms the attachment's four vertices to world coordinates.
-	 *
-	 * See [World transforms](http://esotericsoftware.com/spine-runtime-skeletons#World-transforms) in the Spine
+	/** Transforms the attachment's four vertices to world coordinates. If the attachment has a {@link #sequence}, the region may
+	 * be changed.
+	 * <p>
+	 * See <a href="http://esotericsoftware.com/spine-runtime-skeletons#World-transforms">World transforms</a> 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. */
-	computeWorldVertices (bone: Bone, worldVertices: NumberArrayLike, offset: number, stride: number) {
+	 * @param worldVertices The output world vertices. Must have a length >= <code>offset</code> + 8.
+	 * @param offset The <code>worldVertices</code> index to begin writing values.
+	 * @param stride The number of <code>worldVertices</code> entries between the value pairs written. */
+	computeWorldVertices (slot: Slot, worldVertices: NumberArrayLike, offset: number, stride: number) {
+		if (this.sequence != null) this.sequence.apply(slot, this);
+
+		let bone = slot.bone;
 		let vertexOffset = this.offset;
 		let x = bone.worldX, y = bone.worldY;
 		let a = bone.a, b = bone.b, c = bone.c, d = bone.d;
@@ -187,6 +192,7 @@ export class RegionAttachment extends Attachment {
 		Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, 8);
 		Utils.arrayCopy(this.offset, 0, copy.offset, 0, 8);
 		copy.color.setFromColor(this.color);
+		copy.sequence = this.sequence.copy();
 		return copy;
 	}
 

+ 102 - 0
spine-ts/spine-core/src/attachments/Sequence.ts

@@ -0,0 +1,102 @@
+/******************************************************************************
+ * Spine Runtimes License Agreement
+ * Last updated September 24, 2021. Replaces all prior versions.
+ *
+ * Copyright (c) 2013-2021, 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.
+ *****************************************************************************/
+
+import { TextureRegion } from "../Texture";
+import { Slot } from "../Slot";
+import { HasTextureRegion } from "./HasTextureRegion";
+import { Utils } from "src";
+
+
+export class Sequence {
+	private static _nextID = 0;
+
+	id = Sequence.nextID();
+	regions: TextureRegion[];
+	start = 0;
+	digits = 0;
+	/** The index of the region to show for the setup pose. */
+	setupIndex = 0;
+
+	constructor (count: number) {
+		this.regions = new Array<TextureRegion>(count);
+	}
+
+	copy (): Sequence {
+		let copy = new Sequence(this.regions.length);
+		Utils.arrayCopy(this.regions, 0, copy.regions, 0, this.regions.length);
+		copy.start = this.start;
+		copy.digits = this.digits;
+		copy.setupIndex = this.setupIndex;
+		return copy;
+	}
+
+	apply (slot: Slot, attachment: HasTextureRegion) {
+		let index = slot.sequenceIndex;
+		if (index == -1) index = this.setupIndex;
+		if (index >= this.regions.length) index = this.regions.length - 1;
+		let region = this.regions[index];
+		if (attachment.region != region) {
+			attachment.region = region;
+			attachment.updateRegion();
+		}
+	}
+
+	getPath (basePath: string, index: number): string {
+		let result = basePath;
+		let frame = (this.start + index).toString();
+		for (let i = this.digits - frame.length; i > 0; i--)
+			result += "0";
+		result += frame;
+		return result;
+	}
+
+	private static nextID (): number {
+		return Sequence._nextID++;
+	}
+}
+
+export enum SequenceMode {
+	hold = 0,
+	once = 1,
+	loop = 2,
+	pingpong = 3,
+	onceReverse = 4,
+	loopReverse = 5,
+	pingpongReverse = 6
+}
+
+export const SequenceModeValues = [
+	SequenceMode.hold,
+	SequenceMode.once,
+	SequenceMode.loop,
+	SequenceMode.pingpong,
+	SequenceMode.onceReverse,
+	SequenceMode.loopReverse,
+	SequenceMode.pingpongReverse
+];

+ 1 - 1
spine-ts/spine-threejs/src/SkeletonMesh.ts

@@ -176,7 +176,7 @@ export class SkeletonMesh extends THREE.Object3D {
 				attachmentColor = region.color;
 				vertices = this.vertices;
 				numFloats = vertexSize * 4;
-				region.computeWorldVertices(slot.bone, vertices, 0, vertexSize);
+				region.computeWorldVertices(slot, vertices, 0, vertexSize);
 				triangles = SkeletonMesh.QUAD_TRIANGLES;
 				uvs = region.uvs;
 				texture = <ThreeJsTexture>(<TextureAtlasRegion>region.region.renderObject).page.texture;

+ 1 - 1
spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts

@@ -92,7 +92,7 @@ export class SkeletonDebugRenderer implements Disposable {
 				if (attachment instanceof RegionAttachment) {
 					let regionAttachment = <RegionAttachment>attachment;
 					let vertices = this.vertices;
-					regionAttachment.computeWorldVertices(slot.bone, vertices, 0, 2);
+					regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
 					shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
 					shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
 					shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);

+ 1 - 1
spine-ts/spine-webgl/src/SkeletonRenderer.ts

@@ -109,7 +109,7 @@ export class SkeletonRenderer {
 				renderable.vertices = this.vertices;
 				renderable.numVertices = 4;
 				renderable.numFloats = clippedVertexSize << 2;
-				region.computeWorldVertices(slot.bone, renderable.vertices, 0, clippedVertexSize);
+				region.computeWorldVertices(slot, renderable.vertices, 0, clippedVertexSize);
 				triangles = SkeletonRenderer.QUAD_TRIANGLES;
 				uvs = region.uvs;
 				texture = <GLTexture>(<TextureAtlasRegion>region.region.renderObject).page.texture;

+ 1 - 1
spine-ts/spine-webgl/tests/test-drawcalls.html

@@ -98,7 +98,7 @@
 
 	// Create the Spine canvas which runs the app
 	new spine.SpineCanvas(document.getElementById("canvas"), {
-		pathPrefix: "assets/",
+		pathPrefix: "../example/assets/",
 		app: new App()
 	});
 </script>