mesh.region?.texture).getImage() as HTMLImageElement;
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+
+ const uvs = sequence.getUVs(sequenceIndex);
+ vertices = this.computeMeshVertices(slot, attachment, uvs, false);
+ triangles = attachment.triangles;
+
+ texture = (sequence.regions[sequenceIndex]?.texture as CanvasTexture).getImage();
} else
continue;
@@ -226,7 +241,7 @@ export class SkeletonRenderer {
ctx.restore();
}
- private computeRegionVertices (slot: Slot, region: RegionAttachment, pma: boolean) {
+ private computeRegionVertices (slot: Slot, region: RegionAttachment, offsets: NumberArrayLike, uvs: NumberArrayLike, pma: boolean) {
const skeletonColor = slot.skeleton.color;
const slotColor = slot.applied.color;
const regionColor = region.color;
@@ -238,10 +253,9 @@ export class SkeletonRenderer {
skeletonColor.b * slotColor.b * regionColor.b * multiplier,
alpha);
- region.computeWorldVertices(slot, this.vertices, 0, SkeletonRenderer.VERTEX_SIZE);
+ region.computeWorldVertices(slot, offsets, this.vertices, 0, SkeletonRenderer.VERTEX_SIZE);
const vertices = this.vertices;
- const uvs = region.uvs;
vertices[RegionAttachment.C1R] = color.r;
vertices[RegionAttachment.C1G] = color.g;
@@ -274,7 +288,7 @@ export class SkeletonRenderer {
return vertices;
}
- private computeMeshVertices (slot: Slot, mesh: MeshAttachment, pma: boolean) {
+ private computeMeshVertices (slot: Slot, mesh: MeshAttachment, uvs: NumberArrayLike, pma: boolean) {
const skeleton = slot.skeleton;
const skeletonColor = skeleton.color;
const slotColor = slot.applied.color;
@@ -292,7 +306,7 @@ export class SkeletonRenderer {
if (vertices.length < mesh.worldVerticesLength) this.vertices = vertices = Utils.newFloatArray(mesh.worldVerticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE);
- const uvs = mesh.uvs;
+
for (let i = 0, u = 0, v = 2; i < vertexCount; i++) {
vertices[v++] = color.r;
vertices[v++] = color.g;
diff --git a/spine-ts/spine-core/src/Animation.ts b/spine-ts/spine-core/src/Animation.ts
index 2fc2c98c0..1f7d8c85d 100644
--- a/spine-ts/spine-core/src/Animation.ts
+++ b/spine-ts/spine-core/src/Animation.ts
@@ -28,7 +28,7 @@
*****************************************************************************/
import { type Attachment, VertexAttachment } from "./attachments/Attachment.js";
-import type { HasTextureRegion } from "./attachments/HasTextureRegion.js";
+import { type HasSequence, isHasSequence } from "./attachments/HasSequence.js";
import { SequenceMode, SequenceModeValues } from "./attachments/Sequence.js";
import type { Inherit } from "./BoneData.js";
import type { BoneLocal } from "./BoneLocal.js";
@@ -933,8 +933,8 @@ export class RGBATimeline extends SlotCurveTimeline {
}
protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
- const frames = this.frames;
const color = pose.color;
+ const frames = this.frames;
if (time < frames[0]) {
const setup = slot.data.setup.color;
switch (blend) {
@@ -977,8 +977,12 @@ export class RGBATimeline extends SlotCurveTimeline {
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);
+ if (blend === MixBlend.setup) {
+ const 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);
}
}
}
@@ -1003,64 +1007,67 @@ export class RGBTimeline extends SlotCurveTimeline {
}
protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
- const frames = this.frames;
const color = pose.color;
+ let r = 0, g = 0, b = 0;
+ const frames = this.frames;
if (time < frames[0]) {
const setup = slot.data.setup.color;
switch (blend) {
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.setup:
color.r = setup.r;
color.g = setup.g;
color.b = setup.b;
+ // Fall through.
+ // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
+ default:
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;
}
- return;
- }
-
- let r = 0, g = 0, b = 0;
- const i = Timeline.search(frames, time, 4/*ENTRIES*/);
- const curveType = this.curves[i >> 2];
- switch (curveType) {
- case 0/*LINEAR*/: {
- const before = frames[i];
- r = frames[i + 1/*R*/];
- g = frames[i + 2/*G*/];
- b = frames[i + 3/*B*/];
- const t = (time - before) / (frames[i + 4/*ENTRIES*/] - before);
- r += (frames[i + 4/*ENTRIES*/ + 1/*R*/] - r) * t;
- g += (frames[i + 4/*ENTRIES*/ + 2/*G*/] - g) * t;
- b += (frames[i + 4/*ENTRIES*/ + 3/*B*/] - b) * t;
- break;
- }
- case 1/*STEPPED*/:
- r = frames[i + 1/*R*/];
- g = frames[i + 2/*G*/];
- b = frames[i + 3/*B*/];
- break;
- default:
- r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/);
- g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
- b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
- }
- if (alpha === 1) {
- color.r = r;
- color.g = g;
- color.b = b;
} else {
- if (blend === MixBlend.setup) {
- const setup = slot.data.setup.color;
- color.r = setup.r;
- color.g = setup.g;
- color.b = setup.b;
+ const i = Timeline.search(frames, time, 4/*ENTRIES*/);
+ const curveType = this.curves[i >> 2];
+ switch (curveType) {
+ case 0/*LINEAR*/: {
+ const before = frames[i];
+ r = frames[i + 1/*R*/];
+ g = frames[i + 2/*G*/];
+ b = frames[i + 3/*B*/];
+ const t = (time - before) / (frames[i + 4/*ENTRIES*/] - before);
+ r += (frames[i + 4/*ENTRIES*/ + 1/*R*/] - r) * t;
+ g += (frames[i + 4/*ENTRIES*/ + 2/*G*/] - g) * t;
+ b += (frames[i + 4/*ENTRIES*/ + 3/*B*/] - b) * t;
+ break;
+ }
+ case 1/*STEPPED*/:
+ r = frames[i + 1/*R*/];
+ g = frames[i + 2/*G*/];
+ b = frames[i + 3/*B*/];
+ break;
+ default:
+ r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/);
+ g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
+ b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
+ }
+ if (alpha !== 1) {
+ if (blend === MixBlend.setup) {
+ const 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);
}
}
@@ -1080,23 +1087,31 @@ export class AlphaTimeline extends CurveTimeline1 implements SlotTimeline {
if (!slot.bone.active) return;
const color = (appliedPose ? slot.applied : slot.pose).color;
+ let a = 0;
const frames = this.frames;
if (time < frames[0]) {
const setup = slot.data.setup.color;
switch (blend) {
- case MixBlend.setup: color.a = setup.a; break;
- case MixBlend.first: color.a += (setup.a - color.a) * alpha; break;
- }
- return;
- }
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
+ case MixBlend.setup:
+ color.a = setup.a;
+ // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
+ default:
+ return;
+ case MixBlend.first: a = color.a + (setup.a - color.a) * alpha; break;
- const a = this.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;
+ }
+ } else {
+ a = this.getCurveValue(time);
+ if (alpha !== 1) {
+ if (blend === MixBlend.setup) {
+ const 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);
}
}
@@ -1127,92 +1142,97 @@ export class RGBA2Timeline extends SlotCurveTimeline {
}
protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
- const frames = this.frames;
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
const light = pose.color, dark = pose.darkColor!;
+ let r2 = 0, g2 = 0, b2 = 0
+ const frames = this.frames;
if (time < frames[0]) {
const setup = slot.data.setup;
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
const setupLight = setup.color, setupDark = setup.darkColor!;
switch (blend) {
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.setup:
light.setFromColor(setupLight);
dark.r = setupDark.r;
dark.g = setupDark.g;
dark.b = setupDark.b;
+ // Fall through.
+ // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
+ default:
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;
}
- return;
- }
-
- let r = 0, g = 0, b = 0, a = 0, r2 = 0, g2 = 0, b2 = 0;
- const i = Timeline.search(frames, time, 8/*ENTRIES*/);
- const curveType = this.curves[i >> 3];
- switch (curveType) {
- case 0/*LINEAR*/: {
- const before = frames[i];
- r = frames[i + 1/*R*/];
- g = frames[i + 2/*G*/];
- b = frames[i + 3/*B*/];
- a = frames[i + 4/*A*/];
- r2 = frames[i + 5/*R2*/];
- g2 = frames[i + 6/*G2*/];
- b2 = frames[i + 7/*B2*/];
- const t = (time - before) / (frames[i + 8/*ENTRIES*/] - before);
- r += (frames[i + 8/*ENTRIES*/ + 1/*R*/] - r) * t;
- g += (frames[i + 8/*ENTRIES*/ + 2/*G*/] - g) * t;
- b += (frames[i + 8/*ENTRIES*/ + 3/*B*/] - b) * t;
- a += (frames[i + 8/*ENTRIES*/ + 4/*A*/] - a) * t;
- r2 += (frames[i + 8/*ENTRIES*/ + 5/*R2*/] - r2) * t;
- g2 += (frames[i + 8/*ENTRIES*/ + 6/*G2*/] - g2) * t;
- b2 += (frames[i + 8/*ENTRIES*/ + 7/*B2*/] - b2) * t;
- break;
- }
- case 1/*STEPPED*/:
- r = frames[i + 1/*R*/];
- g = frames[i + 2/*G*/];
- b = frames[i + 3/*B*/];
- a = frames[i + 4/*A*/];
- r2 = frames[i + 5/*R2*/];
- g2 = frames[i + 6/*G2*/];
- b2 = frames[i + 7/*B2*/];
- break;
- default:
- r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/);
- g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
- b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
- a = this.getBezierValue(time, i, 4/*A*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/);
- r2 = this.getBezierValue(time, i, 5/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/);
- g2 = this.getBezierValue(time, i, 6/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/);
- b2 = this.getBezierValue(time, i, 7/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 6 - 2/*BEZIER*/);
- }
-
- if (alpha === 1) {
- light.set(r, g, b, a);
- dark.r = r2;
- dark.g = g2;
- dark.b = b2;
} else {
- if (blend === MixBlend.setup) {
- const setup = slot.data.setup;
- light.setFromColor(setup.color);
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
- const setupDark = setup.darkColor!;
- dark.r = setupDark.r;
- dark.g = setupDark.g;
- dark.b = setupDark.b;
+ let r = 0, g = 0, b = 0, a = 0;
+ const i = Timeline.search(frames, time, 8/*ENTRIES*/);
+ const curveType = this.curves[i >> 3];
+ switch (curveType) {
+ case 0/*LINEAR*/: {
+ const before = frames[i];
+ r = frames[i + 1/*R*/];
+ g = frames[i + 2/*G*/];
+ b = frames[i + 3/*B*/];
+ a = frames[i + 4/*A*/];
+ r2 = frames[i + 5/*R2*/];
+ g2 = frames[i + 6/*G2*/];
+ b2 = frames[i + 7/*B2*/];
+ const t = (time - before) / (frames[i + 8/*ENTRIES*/] - before);
+ r += (frames[i + 8/*ENTRIES*/ + 1/*R*/] - r) * t;
+ g += (frames[i + 8/*ENTRIES*/ + 2/*G*/] - g) * t;
+ b += (frames[i + 8/*ENTRIES*/ + 3/*B*/] - b) * t;
+ a += (frames[i + 8/*ENTRIES*/ + 4/*A*/] - a) * t;
+ r2 += (frames[i + 8/*ENTRIES*/ + 5/*R2*/] - r2) * t;
+ g2 += (frames[i + 8/*ENTRIES*/ + 6/*G2*/] - g2) * t;
+ b2 += (frames[i + 8/*ENTRIES*/ + 7/*B2*/] - b2) * t;
+ break;
+ }
+ case 1/*STEPPED*/:
+ r = frames[i + 1/*R*/];
+ g = frames[i + 2/*G*/];
+ b = frames[i + 3/*B*/];
+ a = frames[i + 4/*A*/];
+ r2 = frames[i + 5/*R2*/];
+ g2 = frames[i + 6/*G2*/];
+ b2 = frames[i + 7/*B2*/];
+ break;
+ default:
+ r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/);
+ g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
+ b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
+ a = this.getBezierValue(time, i, 4/*A*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/);
+ r2 = this.getBezierValue(time, i, 5/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/);
+ g2 = this.getBezierValue(time, i, 6/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/);
+ b2 = this.getBezierValue(time, i, 7/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 6 - 2/*BEZIER*/);
+ }
+
+ if (alpha === 1)
+ light.set(r, g, b, a);
+ else if (blend === MixBlend.setup) {
+ const setupPose = slot.data.setup;
+ let 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);
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
+ 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);
}
}
@@ -1241,14 +1261,16 @@ export class RGB2Timeline extends SlotCurveTimeline {
}
protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
- const frames = this.frames;
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
const light = pose.color, dark = pose.darkColor!;
+ let r = 0, g = 0, b = 0, r2 = 0, g2 = 0, b2 = 0
+ const frames = this.frames;
if (time < frames[0]) {
const setup = slot.data.setup;
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
const setupLight = setup.color, setupDark = setup.darkColor!;
switch (blend) {
+ // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.setup:
light.r = setupLight.r;
light.g = setupLight.g;
@@ -1256,82 +1278,84 @@ export class RGB2Timeline extends SlotCurveTimeline {
dark.r = setupDark.r;
dark.g = setupDark.g;
dark.b = setupDark.b;
+ // Fall through.
+ // biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
+ default:
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;
}
- return;
- }
-
- let r = 0, g = 0, b = 0, r2 = 0, g2 = 0, b2 = 0;
- const i = Timeline.search(frames, time, 7/*ENTRIES*/);
- const curveType = this.curves[i / 7/*ENTRIES*/];
- switch (curveType) {
- case 0/*LINEAR*/: {
- const before = frames[i];
- r = frames[i + 1/*R*/];
- g = frames[i + 2/*G*/];
- b = frames[i + 3/*B*/];
- r2 = frames[i + 4/*R2*/];
- g2 = frames[i + 5/*G2*/];
- b2 = frames[i + 6/*B2*/];
- const t = (time - before) / (frames[i + 7/*ENTRIES*/] - before);
- r += (frames[i + 7/*ENTRIES*/ + 1/*R*/] - r) * t;
- g += (frames[i + 7/*ENTRIES*/ + 2/*G*/] - g) * t;
- b += (frames[i + 7/*ENTRIES*/ + 3/*B*/] - b) * t;
- r2 += (frames[i + 7/*ENTRIES*/ + 4/*R2*/] - r2) * t;
- g2 += (frames[i + 7/*ENTRIES*/ + 5/*G2*/] - g2) * t;
- b2 += (frames[i + 7/*ENTRIES*/ + 6/*B2*/] - b2) * t;
- break;
- }
- case 1/*STEPPED*/:
- r = frames[i + 1/*R*/];
- g = frames[i + 2/*G*/];
- b = frames[i + 3/*B*/];
- r2 = frames[i + 4/*R2*/];
- g2 = frames[i + 5/*G2*/];
- b2 = frames[i + 6/*B2*/];
- break;
- default:
- r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/);
- g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
- b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
- r2 = this.getBezierValue(time, i, 4/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/);
- g2 = this.getBezierValue(time, i, 5/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/);
- b2 = this.getBezierValue(time, i, 6/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*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) {
- const setup = slot.data.setup;
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
- const 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;
+ const i = Timeline.search(frames, time, 7/*ENTRIES*/);
+ const curveType = this.curves[i / 7/*ENTRIES*/];
+ switch (curveType) {
+ case 0/*LINEAR*/: {
+ const before = frames[i];
+ r = frames[i + 1/*R*/];
+ g = frames[i + 2/*G*/];
+ b = frames[i + 3/*B*/];
+ r2 = frames[i + 4/*R2*/];
+ g2 = frames[i + 5/*G2*/];
+ b2 = frames[i + 6/*B2*/];
+ const t = (time - before) / (frames[i + 7/*ENTRIES*/] - before);
+ r += (frames[i + 7/*ENTRIES*/ + 1/*R*/] - r) * t;
+ g += (frames[i + 7/*ENTRIES*/ + 2/*G*/] - g) * t;
+ b += (frames[i + 7/*ENTRIES*/ + 3/*B*/] - b) * t;
+ r2 += (frames[i + 7/*ENTRIES*/ + 4/*R2*/] - r2) * t;
+ g2 += (frames[i + 7/*ENTRIES*/ + 5/*G2*/] - g2) * t;
+ b2 += (frames[i + 7/*ENTRIES*/ + 6/*B2*/] - b2) * t;
+ break;
+ }
+ case 1/*STEPPED*/:
+ r = frames[i + 1/*R*/];
+ g = frames[i + 2/*G*/];
+ b = frames[i + 3/*B*/];
+ r2 = frames[i + 4/*R2*/];
+ g2 = frames[i + 5/*G2*/];
+ b2 = frames[i + 6/*B2*/];
+ break;
+ default:
+ r = this.getBezierValue(time, i, 1/*R*/, curveType - 2/*BEZIER*/);
+ g = this.getBezierValue(time, i, 2/*G*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
+ b = this.getBezierValue(time, i, 3/*B*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
+ r2 = this.getBezierValue(time, i, 4/*R2*/, curveType + 18/*BEZIER_SIZE*/ * 3 - 2/*BEZIER*/);
+ g2 = this.getBezierValue(time, i, 5/*G2*/, curveType + 18/*BEZIER_SIZE*/ * 4 - 2/*BEZIER*/);
+ b2 = this.getBezierValue(time, i, 6/*B2*/, curveType + 18/*BEZIER_SIZE*/ * 5 - 2/*BEZIER*/);
+ }
+
+ if (alpha !== 1) {
+ if (blend === MixBlend.setup) {
+ const setupPose = slot.data.setup;
+ let setup = setupPose.color;
+ r = setup.r + (r - setup.r) * alpha;
+ g = setup.g + (g - setup.g) * alpha;
+ b = setup.b + (b - setup.b) * alpha;
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
+ 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);
}
}
@@ -1619,10 +1643,10 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
static DELAY = 2;
readonly slotIndex: number;
- readonly attachment: HasTextureRegion;
+ readonly attachment: HasSequence;
- constructor (frameCount: number, slotIndex: number, attachment: HasTextureRegion) {
- // biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
+ constructor (frameCount: number, slotIndex: number, attachment: HasSequence) {
+ // biome-ignore lint/style/noNonNullAssertion: reference runtime
super(frameCount, `${Property.sequence}|${slotIndex}|${attachment.sequence!.id}`);
this.slotIndex = slotIndex;
this.attachment = attachment;
@@ -1636,6 +1660,9 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
return this.slotIndex;
}
+ /** The attachment for which the {@link SlotPose#getSequenceIndex()} will be set.
+ *
+ * See {@link VertexAttachment.timelineAttachment}. */
getAttachment () {
return this.attachment as unknown as Attachment;
}
@@ -1658,15 +1685,10 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
if (!slot.bone.active) return;
const pose = appliedPose ? slot.applied : slot.pose;
- const slotAttachment = pose.attachment;
+ const slotAttachment = pose.attachment as Attachment;
const attachment = this.attachment as unknown as Attachment;
- if (slotAttachment !== attachment) {
- if (!(slotAttachment instanceof VertexAttachment)
- || slotAttachment.timelineAttachment !== attachment) return;
- }
- const sequence = (slotAttachment as unknown as HasTextureRegion).sequence;
- if (!sequence) return;
+ if (!(isHasSequence(slotAttachment)) || slotAttachment.timelineAttachment !== attachment) return;
if (direction === MixDirection.out) {
if (blend === MixBlend.setup) pose.sequenceIndex = -1;
@@ -1684,7 +1706,7 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
const modeAndIndex = frames[i + SequenceTimeline.MODE];
const delay = frames[i + SequenceTimeline.DELAY];
- let index = modeAndIndex >> 4, count = sequence.regions.length;
+ let index = modeAndIndex >> 4, count = slotAttachment.sequence.regions.length;
const mode = SequenceModeValues[modeAndIndex & 0xf];
if (mode !== SequenceMode.hold) {
index += (((time - before) / delay + 0.00001) | 0);
@@ -1904,30 +1926,20 @@ export class IkConstraintTimeline extends CurveTimeline implements ConstraintTim
softness = this.getBezierValue(time, i, 2/*SOFTNESS*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
}
- switch (blend) {
- case MixBlend.setup: {
- const setup = constraint.data.setup;
- pose.mix = setup.mix + (mix - setup.mix) * alpha;
- pose.softness = setup.softness + (softness - setup.softness) * alpha;
- if (direction === MixDirection.out) {
- pose.bendDirection = setup.bendDirection;
- pose.compress = setup.compress;
- pose.stretch = setup.stretch;
- return;
- }
- break;
+ if (blend === MixBlend.setup) {
+ const setup = constraint.data.setup;
+ pose.mix = setup.mix + (mix - setup.mix) * alpha;
+ pose.softness = setup.softness + (softness - setup.softness) * alpha;
+ if (direction === MixDirection.out) {
+ pose.bendDirection = setup.bendDirection;
+ pose.compress = setup.compress;
+ pose.stretch = setup.stretch;
+ return;
}
- case MixBlend.first:
- case MixBlend.replace:
- pose.mix += (mix - pose.mix) * alpha;
- pose.softness += (softness - pose.softness) * alpha;
- if (direction === MixDirection.out) return;
- break;
- case MixBlend.add:
- pose.mix += mix * alpha;
- pose.softness += softness * alpha;
- if (direction === MixDirection.out) return;
- break;
+ } else {
+ pose.mix += (mix - pose.mix) * alpha;
+ pose.softness += (softness - pose.softness) * alpha;
+ if (direction === MixDirection.out) return;
}
pose.bendDirection = frames[i + 3/*BEND_DIRECTION*/];
pose.compress = frames[i + 4/*COMPRESS*/] !== 0;
@@ -2105,7 +2117,8 @@ export class PathConstraintSpacingTimeline extends ConstraintTimeline1 {
const constraint = skeleton.constraints[this.constraintIndex];
if (constraint.active) {
const pose = appliedPose ? constraint.applied : constraint.pose;
- pose.spacing = this.getAbsoluteValue(time, alpha, blend, pose.spacing, constraint.data.setup.spacing);
+ pose.spacing = this.getAbsoluteValue(time, alpha, blend === MixBlend.add ? MixBlend.replace : blend, pose.spacing,
+ constraint.data.setup.spacing);
}
}
}
@@ -2186,31 +2199,23 @@ export class PathConstraintMixTimeline extends CurveTimeline implements Constrai
y = this.getBezierValue(time, i, 3/*Y*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
}
- switch (blend) {
- case MixBlend.setup: {
- const 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;
- break;
- }
- case MixBlend.first:
- case MixBlend.replace:
- pose.mixRotate += (rotate - pose.mixRotate) * alpha;
- pose.mixX += (x - pose.mixX) * alpha;
- pose.mixY += (y - pose.mixY) * alpha;
- break;
- case MixBlend.add:
- pose.mixRotate += rotate * alpha;
- pose.mixX += x * alpha;
- pose.mixY += y * alpha;
- break;
+ if (blend === MixBlend.setup) {
+ const 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;
}
}
}
/** The base class for most {@link PhysicsConstraint} timelines. */
export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
+ additive = false;
+
/** @param constraintIndex -1 for all physics constraints in the skeleton. */
constructor (frameCount: number, bezierCount: number, constraintIndex: number, property: number) {
super(frameCount, bezierCount, constraintIndex, property);
@@ -2219,6 +2224,7 @@ export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array, alpha: number, blend: MixBlend,
direction: MixDirection, appliedPose: boolean) {
+ if (blend === MixBlend.add && !this.additive) blend = MixBlend.replace;
if (this.constraintIndex === -1) {
const value = time >= this.frames[0] ? this.getCurveValue(time) : 0;
const constraints = skeleton.physics;
@@ -2323,6 +2329,7 @@ export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline {
export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
constructor (frameCount: number, bezierCount: number, constraintIndex: number) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind);
+ this.additive = true;
}
get (pose: PhysicsConstraintPose): number {
@@ -2342,6 +2349,7 @@ export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline {
constructor (frameCount: number, bezierCount: number, constraintIndex: number) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity);
+ this.additive = true;
}
get (pose: PhysicsConstraintPose): number {
diff --git a/spine-ts/spine-core/src/AnimationState.ts b/spine-ts/spine-core/src/AnimationState.ts
index 19dc89d96..3aa80d85b 100644
--- a/spine-ts/spine-core/src/AnimationState.ts
+++ b/spine-ts/spine-core/src/AnimationState.ts
@@ -198,7 +198,7 @@ export class AnimationState {
Utils.webkit602BugfixHelper(alpha, blend);
const timeline = timelines[ii];
if (timeline instanceof AttachmentTimeline)
- this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments);
+ this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, false, attachments);
else
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false);
}
@@ -215,7 +215,7 @@ export class AnimationState {
if (!shortestRotation && timeline instanceof RotateTimeline) {
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, current.timelinesRotation, ii << 1, firstFrame);
} else if (timeline instanceof AttachmentTimeline) {
- this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments);
+ this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, false, attachments);
} else {
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
Utils.webkit602BugfixHelper(alpha, blend);
@@ -286,7 +286,6 @@ export class AnimationState {
from.totalAlpha = 0;
for (let i = 0; i < timelineCount; i++) {
const timeline = timelines[i];
- let direction = MixDirection.out;
let timelineBlend: MixBlend;
let alpha = 0;
switch (timelineMode[i]) {
@@ -319,8 +318,9 @@ export class AnimationState {
if (!shortestRotation && timeline instanceof RotateTimeline)
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, from.timelinesRotation, i << 1, firstFrame);
else if (timeline instanceof AttachmentTimeline)
- this.applyAttachmentTimeline(timeline, skeleton, applyTime, timelineBlend, attachments && alpha >= from.alphaAttachmentThreshold);
+ this.applyAttachmentTimeline(timeline, skeleton, applyTime, timelineBlend, true, attachments && alpha >= from.alphaAttachmentThreshold);
else {
+ let direction = MixDirection.out;
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
Utils.webkit602BugfixHelper(alpha, blend);
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend === MixBlend.setup)
@@ -338,11 +338,13 @@ export class AnimationState {
return mix;
}
- applyAttachmentTimeline (timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, attachments: boolean) {
+ applyAttachmentTimeline (timeline: AttachmentTimeline, skeleton: Skeleton, time: number, blend: MixBlend, out: boolean, attachments: boolean) {
const 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
@@ -965,7 +967,8 @@ export class TrackEntry {
* to 1, which overwrites the skeleton's current pose with this animation.
*
* Typically track 0 is used to completely pose the skeleton, then alpha is used on higher tracks. It doesn't make sense to
- * use alpha on track 0 if the skeleton pose is from the last frame render. */
+ * use alpha on track 0 if the skeleton pose is from the last frame render.
+ * @see alphaAttachmentThreshold */
alpha: number = 0;
/** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be
diff --git a/spine-ts/spine-core/src/AtlasAttachmentLoader.ts b/spine-ts/spine-core/src/AtlasAttachmentLoader.ts
index db1b0c9f6..608ec8aa8 100644
--- a/spine-ts/spine-core/src/AtlasAttachmentLoader.ts
+++ b/spine-ts/spine-core/src/AtlasAttachmentLoader.ts
@@ -51,40 +51,27 @@ export class AtlasAttachmentLoader implements AttachmentLoader {
this.allowMissingRegions = allowMissingRegions;
}
- loadSequence (name: string, basePath: string, sequence: Sequence) {
+ protected findRegions (name: string, basePath: string, sequence: Sequence) {
const regions = sequence.regions;
- for (let i = 0, n = regions.length; i < n; i++) {
- const path = sequence.getPath(basePath, i);
- regions[i] = this.atlas.findRegion(path);
- if (regions[i] == null && !this.allowMissingRegions)
- throw new Error(`Region not found in atlas: ${path} (sequence: ${name})`);
- }
+ for (let i = 0, n = regions.length; i < n; i++)
+ regions[i] = this.findRegion(name, sequence.getPath(basePath, i));
+ }
+
+ protected findRegion (name: string, path: string) {
+ const region = this.atlas.findRegion(path);
+ if (!region && !this.allowMissingRegions)
+ throw new Error(`Region not found in atlas: ${path} (attachment: ${name})`);
+ return region;
}
newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence): RegionAttachment {
- const attachment = new RegionAttachment(name, path);
- if (sequence != null) {
- this.loadSequence(name, path, sequence);
- } else {
- const region = this.atlas.findRegion(path);
- if (region == null && !this.allowMissingRegions)
- throw new Error(`Region not found in atlas: ${path} (region attachment: ${name})`);
- attachment.region = region;
- }
- return attachment;
+ this.findRegions(name, path, sequence);
+ return new RegionAttachment(name, sequence);
}
newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment {
- const attachment = new MeshAttachment(name, path);
- if (sequence != null) {
- this.loadSequence(name, path, sequence);
- } else {
- const region = this.atlas.findRegion(path);
- if (region == null && !this.allowMissingRegions)
- throw new Error(`Region not found in atlas: ${path} (mesh attachment: ${name})`);
- attachment.region = region;
- }
- return attachment;
+ this.findRegions(name, path, sequence);
+ return new MeshAttachment(name, sequence);
}
newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment {
diff --git a/spine-ts/spine-core/src/IkConstraint.ts b/spine-ts/spine-core/src/IkConstraint.ts
index 84ad2d44c..39bee393d 100644
--- a/spine-ts/spine-core/src/IkConstraint.ts
+++ b/spine-ts/spine-core/src/IkConstraint.ts
@@ -210,7 +210,7 @@ export class IkConstraint extends Constraint> 1) & 2;
- data.spacingMode = (flags >> 2) & 3;
- data.rotateMode = (flags >> 4) & 3;
+ data.positionMode = (flags >> 1) & 0b1;
+ data.spacingMode = (flags >> 2) & 0b11;
+ data.rotateMode = (flags >> 4) & 0b11;
if ((flags & 128) !== 0) data.offsetRotation = input.readFloat();
const setup = data.setup;
setup.position = input.readFloat();
@@ -375,7 +375,7 @@ export class SkeletonBinary {
if (!parent) throw new Error(`Parent mesh not found: ${linkedMesh.parent}`);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? parent as VertexAttachment : linkedMesh.mesh;
linkedMesh.mesh.setParentMesh(parent as MeshAttachment);
- if (linkedMesh.mesh.region != null) linkedMesh.mesh.updateRegion();
+ linkedMesh.mesh.updateSequence();
}
this.linkedMeshes.length = 0;
@@ -465,7 +465,7 @@ export class SkeletonBinary {
case AttachmentType.Region: {
let path = (flags & 16) !== 0 ? input.readStringRef() : null;
const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff;
- const sequence = (flags & 64) !== 0 ? this.readSequence(input) : null;
+ const sequence = this.readSequence(input, (flags & 64) !== 0);
const rotation = (flags & 128) !== 0 ? input.readFloat() : 0;
const x = input.readFloat();
const y = input.readFloat();
@@ -486,8 +486,7 @@ export class SkeletonBinary {
region.width = width * scale;
region.height = height * scale;
Color.rgba8888ToColor(region.color, color);
- region.sequence = sequence;
- if (region.region != null) region.updateRegion();
+ region.updateSequence();
return region;
}
case AttachmentType.BoundingBox: {
@@ -505,7 +504,7 @@ export class SkeletonBinary {
case AttachmentType.Mesh: {
let path = (flags & 16) !== 0 ? input.readStringRef() : name;
const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff;
- const sequence = (flags & 64) !== 0 ? this.readSequence(input) : null;
+ const sequence = this.readSequence(input, (flags & 64) !== 0);
const hullLength = input.readInt(true);
const vertices = this.readVertices(input, (flags & 128) !== 0);
const uvs = this.readFloatArray(input, vertices.length, 1);
@@ -523,26 +522,25 @@ export class SkeletonBinary {
if (!mesh) return null;
mesh.path = path;
Color.rgba8888ToColor(mesh.color, color);
+ mesh.hullLength = hullLength << 1;
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: {
const path = (flags & 16) !== 0 ? input.readStringRef() : name;
if (path == null) throw new Error("Path of linked mesh must not be null");
const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff;
- const sequence = (flags & 64) !== 0 ? this.readSequence(input) : null;
+ const sequence = this.readSequence(input, (flags & 64) !== 0);
const inheritTimelines = (flags & 128) !== 0;
const skinIndex = input.readInt(true);
const parent = input.readStringRef();
@@ -556,7 +554,6 @@ export class SkeletonBinary {
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;
@@ -616,8 +613,9 @@ export class SkeletonBinary {
}
}
- private readSequence (input: BinaryInput) {
- const sequence = new Sequence(input.readInt(true));
+ private readSequence (input: BinaryInput, hasPathSuffix: boolean) {
+ if (!hasPathSuffix) return new Sequence(1, false);
+ const sequence = new Sequence(input.readInt(true), true);
sequence.start = input.readInt(true);
sequence.digits = input.readInt(true);
sequence.setupIndex = input.readInt(true);
@@ -632,19 +630,20 @@ export class SkeletonBinary {
if (!weighted)
return new Vertices(null, this.readFloatArray(input, length, scale), length);
+ const n = input.readInt(true);
+ const bones: number[] = [];
const weights: number[] = [];
- const bonesArray: number[] = [];
- for (let i = 0; i < vertexCount; i++) {
+ for (let b = 0, w = 0; b < n;) {
const boneCount = input.readInt(true);
- bonesArray.push(boneCount);
- for (let ii = 0; ii < boneCount; ii++) {
- bonesArray.push(input.readInt(true));
- weights.push(input.readFloat() * scale);
- weights.push(input.readFloat() * scale);
- weights.push(input.readFloat());
+ bones[b++] = boneCount;
+ for (let ii = 0; ii < boneCount; ii++, w += 3) {
+ bones[b++] = input.readInt(true);
+ weights[w] = input.readFloat() * scale;
+ weights[w + 1] = input.readFloat() * scale;
+ weights[w + 2] = input.readFloat();
}
}
- return new Vertices(bonesArray, Utils.toFloatArray(weights), length);
+ return new Vertices(bones, Utils.toFloatArray(weights), length);
}
private readFloatArray (input: BinaryInput, n: number, scale: number): number[] {
@@ -1115,7 +1114,7 @@ export class SkeletonBinary {
break;
}
case ATTACHMENT_SEQUENCE: {
- const timeline = new SequenceTimeline(frameCount, slotIndex, attachment as unknown as HasTextureRegion);
+ const timeline = new SequenceTimeline(frameCount, slotIndex, attachment as unknown as HasSequence);
for (let frame = 0; frame < frameCount; frame++) {
const time = input.readFloat();
const modeAndIndex = input.readInt32();
diff --git a/spine-ts/spine-core/src/SkeletonJson.ts b/spine-ts/spine-core/src/SkeletonJson.ts
index f417f58cd..7d9e9a6bf 100644
--- a/spine-ts/spine-core/src/SkeletonJson.ts
+++ b/spine-ts/spine-core/src/SkeletonJson.ts
@@ -30,7 +30,7 @@
import { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, type CurveTimeline1, DeformTimeline, DrawOrderTimeline, EventTimeline, IkConstraintTimeline, InheritTimeline, PathConstraintMixTimeline, PathConstraintPositionTimeline, PathConstraintSpacingTimeline, PhysicsConstraintDampingTimeline, PhysicsConstraintGravityTimeline, PhysicsConstraintInertiaTimeline, PhysicsConstraintMassTimeline, PhysicsConstraintMixTimeline, PhysicsConstraintResetTimeline, PhysicsConstraintStrengthTimeline, PhysicsConstraintWindTimeline, RGB2Timeline, RGBA2Timeline, RGBATimeline, RGBTimeline, RotateTimeline, ScaleTimeline, ScaleXTimeline, ScaleYTimeline, SequenceTimeline, ShearTimeline, ShearXTimeline, ShearYTimeline, SliderMixTimeline, SliderTimeline, type Timeline, TransformConstraintTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline } from "./Animation.js";
import type { Attachment, VertexAttachment } from "./attachments/Attachment.js";
import type { AttachmentLoader } from "./attachments/AttachmentLoader.js";
-import type { HasTextureRegion } from "./attachments/HasTextureRegion.js";
+import type { HasSequence } from "./attachments/HasSequence.js";
import type { MeshAttachment } from "./attachments/MeshAttachment.js";
import { Sequence, SequenceMode } from "./attachments/Sequence.js";
import { BoneData, Inherit } from "./BoneData.js";
@@ -440,7 +440,7 @@ export class SkeletonJson {
if (!parent) throw new Error(`Parent mesh not found: ${linkedMesh.parent}`);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? parent : linkedMesh.mesh;
linkedMesh.mesh.setParentMesh(parent);
- if (linkedMesh.mesh.region != null) linkedMesh.mesh.updateRegion();
+ linkedMesh.mesh.updateSequence();
}
this.linkedMeshes.length = 0;
@@ -528,12 +528,11 @@ export class SkeletonJson {
region.rotation = getValue(map, "rotation", 0);
region.width = map.width * scale;
region.height = map.height * scale;
- region.sequence = sequence;
const color: string = getValue(map, "color", null);
if (color) region.color.setFromString(color);
- if (region.region != null) region.updateRegion();
+ region.updateSequence();
return region;
}
case "boundingbox": {
@@ -557,7 +556,6 @@ export class SkeletonJson {
mesh.width = getValue(map, "width", 0) * scale;
mesh.height = getValue(map, "height", 0) * scale;
- mesh.sequence = sequence;
const parent: string = getValue(map, "parent", null);
if (parent) {
@@ -569,10 +567,10 @@ export class SkeletonJson {
this.readVertices(map, mesh, uvs.length);
mesh.triangles = map.triangles;
mesh.regionUVs = uvs;
- if (mesh.region != null) mesh.updateRegion();
mesh.edges = getValue(map, "edges", null);
mesh.hullLength = getValue(map, "hull", 0) * 2;
+ mesh.updateSequence();
return mesh;
}
case "path": {
@@ -623,8 +621,8 @@ export class SkeletonJson {
}
readSequence (map: object) {
- if (map == null) return null;
- const sequence = new Sequence(getValue(map, "count", 0));
+ if (map == null) return new Sequence(1, false);
+ const sequence = new Sequence(getValue(map, "count", 0), true);
sequence.start = getValue(map, "start", 1);
sequence.digits = getValue(map, "digits", 0);
sequence.setupIndex = getValue(map, "setup", 0);
@@ -1155,7 +1153,7 @@ export class SkeletonJson {
}
timelines.push(timeline);
} else if (timelineMapName === "sequence") {
- const timeline = new SequenceTimeline(timelineMap.length, slotIndex, attachment as unknown as HasTextureRegion);
+ const timeline = new SequenceTimeline(timelineMap.length, slotIndex, attachment as unknown as HasSequence);
let lastDelay = 0;
for (let frame = 0; frame < timelineMap.length; frame++) {
const delay = getValue(keyMap, "delay", lastDelay);
diff --git a/spine-ts/spine-core/src/SkeletonRendererCore.ts b/spine-ts/spine-core/src/SkeletonRendererCore.ts
index b4fdc426c..413eb5128 100644
--- a/spine-ts/spine-core/src/SkeletonRendererCore.ts
+++ b/spine-ts/spine-core/src/SkeletonRendererCore.ts
@@ -55,8 +55,8 @@ export class SkeletonRendererCore {
continue;
}
- const slotApplied = slot.applied;
- const slotColor = slotApplied.color;
+ const pose = slot.applied;
+ const slotColor = pose.color;
const alpha = slotColor.a;
if ((alpha === 0 || !slot.bone.active) && !(attachment instanceof ClippingAttachment)) {
clipper.clipEnd(slot);
@@ -80,13 +80,16 @@ export class SkeletonRendererCore {
continue;
}
- attachment.computeWorldVertices(slot, this.worldVertices, 0, stride);
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(pose), this.worldVertices, 0, stride);
+
vertices = this.worldVertices;
verticesCount = 4;
- uvs = attachment.uvs as Float32Array;
+ uvs = sequence.getUVs(sequenceIndex);
indices = this.quadIndices;
indicesCount = 6;
- texture = attachment.region?.texture;
+ texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof MeshAttachment) {
attachmentColor = attachment.color;
@@ -102,10 +105,14 @@ export class SkeletonRendererCore {
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, this.worldVertices, 0, stride);
vertices = this.worldVertices;
verticesCount = attachment.worldVerticesLength >> 1;
- uvs = attachment.uvs as Float32Array;
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+
+ uvs = sequence.getUVs(sequenceIndex);
indices = attachment.triangles;
indicesCount = indices.length;
- texture = attachment.region?.texture;
+ texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof ClippingAttachment) {
clipper.clipStart(skeleton, slot, attachment);
@@ -133,8 +140,8 @@ export class SkeletonRendererCore {
}
darkColor = 0xff000000;
- if (slotApplied.darkColor) {
- const { r, g, b } = slotApplied.darkColor;
+ if (pose.darkColor) {
+ const { r, g, b } = pose.darkColor;
darkColor = 0xff000000 |
(Math.floor(r * a) << 16) |
(Math.floor(g * a) << 8) |
@@ -156,8 +163,8 @@ export class SkeletonRendererCore {
}
darkColor = 0;
- if (slotApplied.darkColor) {
- const { r, g, b } = slotApplied.darkColor;
+ if (pose.darkColor) {
+ const { r, g, b } = pose.darkColor;
darkColor = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255);
}
}
diff --git a/spine-ts/spine-core/src/attachments/Attachment.ts b/spine-ts/spine-core/src/attachments/Attachment.ts
index 1669fffc6..dfba6a725 100644
--- a/spine-ts/spine-core/src/attachments/Attachment.ts
+++ b/spine-ts/spine-core/src/attachments/Attachment.ts
@@ -35,9 +35,14 @@ import { type NumberArrayLike, Utils } from "../Utils.js";
export abstract class Attachment {
name: string;
+ /** Timelines for the timeline attachment are also applied to this attachment.
+ * @return May be null if no attachment-specific timelines should be applied. */
+ timelineAttachment?: Attachment;
+
constructor (name: string) {
if (!name) throw new Error("name cannot be null.");
this.name = name;
+ this.timelineAttachment = this;
}
abstract copy (): Attachment;
@@ -65,10 +70,6 @@ export abstract class VertexAttachment extends Attachment {
* {@link computeWorldVertices} using the `count` parameter. */
worldVerticesLength = 0;
- /** Timelines for the timeline attachment are also applied to this attachment.
- * May be null if no attachment-specific timelines should be applied. */
- timelineAttachment: Attachment = this;
-
constructor (name: string) {
super(name);
}
diff --git a/spine-ts/spine-core/src/attachments/AttachmentLoader.ts b/spine-ts/spine-core/src/attachments/AttachmentLoader.ts
index e0b2e7024..ff49b04eb 100644
--- a/spine-ts/spine-core/src/attachments/AttachmentLoader.ts
+++ b/spine-ts/spine-core/src/attachments/AttachmentLoader.ts
@@ -42,10 +42,10 @@ import type { Sequence } from "./Sequence.js";
* Runtimes Guide. */
export interface AttachmentLoader {
/** @return May be null to not load an attachment. */
- newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence | null): 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, sequence: Sequence | null): 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;
diff --git a/spine-ts/spine-core/src/attachments/HasTextureRegion.ts b/spine-ts/spine-core/src/attachments/HasSequence.ts
similarity index 74%
rename from spine-ts/spine-core/src/attachments/HasTextureRegion.ts
rename to spine-ts/spine-core/src/attachments/HasSequence.ts
index b2fbb2126..50709f313 100644
--- a/spine-ts/spine-core/src/attachments/HasTextureRegion.ts
+++ b/spine-ts/spine-core/src/attachments/HasSequence.ts
@@ -27,24 +27,20 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
-import type { TextureRegion } from "../Texture.js"
import type { Color } from "../Utils.js"
import type { Sequence } from "./Sequence.js"
-export interface HasTextureRegion {
- /** The name used to find the {@link #region()}. */
- path: string;
+export function isHasSequence (obj: unknown): obj is HasSequence {
+ return !!obj && typeof obj === "object" && "sequence" in obj && "updateSequence" in obj;
+}
- /** 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 | null;
+export interface HasSequence {
+ path?: string;
- /** 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 | null;
+ /** Calls {@link Sequence#update(HasSequence)} on this attachment's sequence. */
+ updateSequence (): void;
+
+ sequence: Sequence;
}
diff --git a/spine-ts/spine-core/src/attachments/MeshAttachment.ts b/spine-ts/spine-core/src/attachments/MeshAttachment.ts
index 731e2bc49..56e9eddfc 100644
--- a/spine-ts/spine-core/src/attachments/MeshAttachment.ts
+++ b/spine-ts/spine-core/src/attachments/MeshAttachment.ts
@@ -27,74 +27,123 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
-import type { Skeleton } from "src/Skeleton.js";
-import type { Slot } from "../Slot.js";
import type { TextureRegion } from "../Texture.js";
import { TextureAtlasRegion } from "../TextureAtlas.js";
import { Color, type NumberArrayLike, Utils } from "../Utils.js";
import { type Attachment, VertexAttachment } from "./Attachment.js";
-import type { HasTextureRegion } from "./HasTextureRegion.js";
+import type { HasSequence } from "./HasSequence.js";
import type { Sequence } from "./Sequence.js";
/** 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 implements HasTextureRegion {
- region: TextureRegion | null = null;
-
- /** The name of the texture region for this attachment. */
- path: string;
+export class MeshAttachment extends VertexAttachment implements HasSequence {
+ readonly sequence: Sequence;
/** The UV pair for each vertex, normalized within the texture region. */
regionUVs: NumberArrayLike = [];
- /** The UV pair for each vertex, normalized within the entire texture.
- *
- * See {@link #updateUVs}. */
- uvs: NumberArrayLike = [];
-
/** Triplets of vertex indices which describe the mesh's triangulation. */
triangles: Array = [];
+ /** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */
+ hullLength: number = 0;
+
+ /** The name of the texture region for this attachment. */
+ path?: string;
+
/** The color to tint the mesh. */
color = new Color(1, 1, 1, 1);
+ private parentMesh: MeshAttachment | null = null;
+
+ /** Vertex index pairs describing edges for controlling triangulation, or be null if nonessential data was not exported. Mesh
+ * triangles never cross edges. Triangulation is not performed at runtime. */
+ edges: Array = [];
+
/** The width of the mesh's image. Available only when nonessential data was exported. */
width: number = 0;
/** The height of the mesh's image. Available only when nonessential data was exported. */
height: number = 0;
- /** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */
- hullLength: number = 0;
-
- /** Vertex index pairs describing edges for controling triangulation. Mesh triangles will never cross edges. Only available if
- * nonessential data was exported. Triangulation is not performed at runtime. */
- edges: Array = [];
-
- private parentMesh: MeshAttachment | null = null;
-
- sequence: Sequence | null = null;
-
tempColor = new Color(0, 0, 0, 0);
- constructor (name: string, path: string) {
+ constructor (name: string, sequence: Sequence) {
super(name);
- this.path = path;
+ this.sequence = sequence;
}
- /** 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 () {
- if (!this.region) throw new Error("Region not set.");
- const regionUVs = this.regionUVs;
- if (!this.uvs || this.uvs.length !== regionUVs.length) this.uvs = Utils.newFloatArray(regionUVs.length);
- const uvs = this.uvs;
- const n = this.uvs.length;
- let u = this.region.u, v = this.region.v, width = 0, height = 0;
- if (this.region instanceof TextureAtlasRegion) {
- const region = this.region, page = region.page;
+ copy (): Attachment {
+ if (this.parentMesh) return this.newLinkedMesh();
+
+ const copy = new MeshAttachment(this.name, this.sequence.copy());
+ copy.path = this.path;
+ copy.color.setFromColor(this.color);
+
+ this.copyTo(copy);
+ copy.regionUVs = [];
+ Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length);
+ copy.triangles = [];
+ Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length);
+ copy.hullLength = this.hullLength;
+
+ // Nonessential.
+ if (this.edges) {
+ copy.edges = [];
+ Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length);
+ }
+ copy.width = this.width;
+ copy.height = this.height;
+
+ return copy;
+ }
+
+ updateSequence () {
+ this.sequence.update(this);
+ }
+
+ /** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
+ * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
+ * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
+ getParentMesh () {
+ return this.parentMesh;
+ }
+
+ /** @param parentMesh May be null. */
+ setParentMesh (parentMesh: MeshAttachment) {
+ this.parentMesh = parentMesh;
+ if (parentMesh) {
+ this.bones = parentMesh.bones;
+ this.vertices = parentMesh.vertices;
+ this.worldVerticesLength = parentMesh.worldVerticesLength;
+ this.regionUVs = parentMesh.regionUVs;
+ this.triangles = parentMesh.triangles;
+ this.hullLength = parentMesh.hullLength;
+ this.worldVerticesLength = parentMesh.worldVerticesLength
+ }
+ }
+
+ /** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/
+ newLinkedMesh (): MeshAttachment {
+ const copy = new MeshAttachment(this.name, this.sequence.copy());
+ copy.timelineAttachment = this.timelineAttachment;
+ copy.path = this.path;
+ copy.color.setFromColor(this.color);
+ copy.setParentMesh(this.parentMesh ? this.parentMesh : this);
+ copy.updateSequence();
+ return copy;
+ }
+
+ /** Computes {@link Sequence#getUVs(int) UVs} for a mesh attachment.
+ * @param uvs Output array for the computed UVs, same length as regionUVs. */
+ static computeUVs (region: TextureRegion | null, regionUVs: NumberArrayLike, uvs: NumberArrayLike): void {
+ if (!region) throw new Error("Region not set.");
+ const n = uvs.length;
+ let u = region.u, v = region.v, width = 0, height = 0;
+ if (region instanceof TextureAtlasRegion) {
+ const page = region.page;
const textureWidth = page.width, textureHeight = page.height;
switch (region.degrees) {
case 90:
@@ -133,12 +182,12 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight;
}
- } else if (!this.region) {
+ } else if (!region) {
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;
}
for (let i = 0; i < n; i += 2) {
@@ -146,70 +195,4 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
uvs[i + 1] = v + regionUVs[i + 1] * height;
}
}
-
- /** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
- * {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
- * parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
- getParentMesh () {
- return this.parentMesh;
- }
-
- /** @param parentMesh May be null. */
- setParentMesh (parentMesh: MeshAttachment) {
- this.parentMesh = parentMesh;
- if (parentMesh) {
- this.bones = parentMesh.bones;
- this.vertices = parentMesh.vertices;
- this.worldVerticesLength = parentMesh.worldVerticesLength;
- this.regionUVs = parentMesh.regionUVs;
- this.triangles = parentMesh.triangles;
- this.hullLength = parentMesh.hullLength;
- this.worldVerticesLength = parentMesh.worldVerticesLength
- }
- }
-
- copy (): Attachment {
- if (this.parentMesh) return this.newLinkedMesh();
-
- const copy = new MeshAttachment(this.name, this.path);
- copy.region = this.region;
- copy.color.setFromColor(this.color);
-
- this.copyTo(copy);
- copy.regionUVs = [];
- Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length);
- copy.uvs = this.uvs instanceof Float32Array ? Utils.newFloatArray(this.uvs.length) : [];
- Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, this.uvs.length);
- copy.triangles = [];
- Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length);
- copy.hullLength = this.hullLength;
-
- copy.sequence = this.sequence != null ? this.sequence.copy() : null;
-
- // Nonessential.
- if (this.edges) {
- copy.edges = [];
- Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length);
- }
- copy.width = this.width;
- copy.height = this.height;
-
- return copy;
- }
-
- computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) {
- if (this.sequence != null) this.sequence.apply(slot.applied, this);
- super.computeWorldVertices(skeleton, 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 {
- const copy = new MeshAttachment(this.name, this.path);
- copy.region = this.region;
- copy.color.setFromColor(this.color);
- copy.timelineAttachment = this.timelineAttachment;
- copy.setParentMesh(this.parentMesh ? this.parentMesh : this);
- if (copy.region != null) copy.updateRegion();
- return copy;
- }
}
diff --git a/spine-ts/spine-core/src/attachments/RegionAttachment.ts b/spine-ts/spine-core/src/attachments/RegionAttachment.ts
index a87507abd..326927970 100644
--- a/spine-ts/spine-core/src/attachments/RegionAttachment.ts
+++ b/spine-ts/spine-core/src/attachments/RegionAttachment.ts
@@ -27,17 +27,20 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
+import type { SlotPose } from "src/SlotPose.js";
import type { Slot } from "../Slot.js";
import type { TextureRegion } from "../Texture.js";
-import { Color, MathUtils, type NumberArrayLike, Utils } from "../Utils.js";
+import { Color, MathUtils, type NumberArrayLike } from "../Utils.js";
import { Attachment } from "./Attachment.js";
-import type { HasTextureRegion } from "./HasTextureRegion.js";
+import type { HasSequence } from "./HasSequence.js";
import type { Sequence } from "./Sequence.js";
/** 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 implements HasTextureRegion {
+export class RegionAttachment extends Attachment implements HasSequence {
+ readonly sequence: Sequence;
+
/** The local x translation. */
x = 0;
@@ -59,44 +62,95 @@ export class RegionAttachment extends Attachment implements HasTextureRegion {
/** The height of the region attachment in Spine. */
height = 0;
+ /** The name of the texture region for this attachment. */
+ path?: string;
+
/** The color to tint the region attachment. */
color = new Color(1, 1, 1, 1);
- /** The name of the texture region for this attachment. */
- path: string;
-
- region: TextureRegion | null = null;
- sequence: Sequence | null = null;
-
- /** For each of the 4 vertices, a pair of x,y values that is the local position of the vertex.
- *
- * See {@link #updateRegion()}. */
- offset = Utils.newFloatArray(8);
-
- uvs = Utils.newFloatArray(8);
-
tempColor = new Color(1, 1, 1, 1);
- constructor (name: string, path: string) {
+ constructor (name: string, sequence: Sequence) {
super(name);
- this.path = path;
+ this.sequence = sequence;
}
- /** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */
- updateRegion (): void {
- if (!this.region) throw new Error("Region not set.");
- const region = this.region;
- const uvs = this.uvs;
- const regionScaleX = this.width / this.region.originalWidth * this.scaleX;
- const regionScaleY = this.height / this.region.originalHeight * this.scaleY;
- const localX = -this.width / 2 * this.scaleX + this.region.offsetX * regionScaleX;
- const localY = -this.height / 2 * this.scaleY + this.region.offsetY * regionScaleY;
- const localX2 = localX + this.region.width * regionScaleX;
- const localY2 = localY + this.region.height * regionScaleY;
- const radians = this.rotation * MathUtils.degRad;
+ copy (): Attachment {
+ const copy = new RegionAttachment(this.name, this.sequence.copy());
+ copy.path = this.path;
+ copy.x = this.x;
+ copy.y = this.y;
+ copy.scaleX = this.scaleX;
+ copy.scaleY = this.scaleY;
+ copy.rotation = this.rotation;
+ copy.width = this.width;
+ copy.height = this.height;
+ copy.color.setFromColor(this.color);
+ return copy;
+ }
+
+ /** Transforms the attachment's four vertices to world coordinates.
+ *
+ * See 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. */
+ computeWorldVertices (slot: Slot, vertexOffsets: NumberArrayLike, worldVertices: NumberArrayLike, offset: number, stride: number) {
+
+ const bone = slot.bone.applied;
+ const x = bone.worldX, y = bone.worldY;
+ const a = bone.a, b = bone.b, c = bone.c, d = bone.d;
+
+ let offsetX = vertexOffsets[0];
+ let 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;
+ }
+
+ getOffsets (pose: SlotPose): number[] {
+ // biome-ignore lint/style/noNonNullAssertion: offsets are always defined after updateSequence
+ return this.sequence.offsets![this.sequence.resolveIndex(pose)];
+ }
+
+ updateSequence () {
+ this.sequence.update(this);
+ }
+
+ /** Computes {@link Sequence#getUVs(int) UVs} and {@link Sequence#getOffsets(int) 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. */
+ static computeUVs (region: TextureRegion | null, x: number, y: number, scaleX: number, scaleY: number, rotation: number, width: number,
+ height: number, offset: number[], uvs: NumberArrayLike): void {
+
+ if (!region) throw new Error("Region not set.");
+ const regionScaleX = width / region.originalWidth * scaleX;
+ const regionScaleY = height / region.originalHeight * scaleY;
+ const localX = -width / 2 * scaleX + region.offsetX * regionScaleX;
+ const localY = -height / 2 * scaleY + region.offsetY * regionScaleY;
+ const localX2 = localX + region.width * regionScaleX;
+ const localY2 = localY + region.height * regionScaleY;
+ const radians = rotation * MathUtils.degRad;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
- const x = this.x, y = this.y;
const localXCos = localX * cos + x;
const localXSin = localX * sin;
const localYCos = localY * cos + y;
@@ -105,7 +159,6 @@ export class RegionAttachment extends Attachment implements HasTextureRegion {
const localX2Sin = localX2 * sin;
const localY2Cos = localY2 * cos + y;
const localY2Sin = localY2 * sin;
- const offset = this.offset;
offset[0] = localXCos - localYSin;
offset[1] = localYCos + localXSin;
offset[2] = localXCos - localY2Sin;
@@ -124,85 +177,25 @@ export 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 {@link #sequence}, the region may
- * be changed.
- *
- * See 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. */
- computeWorldVertices (slot: Slot, worldVertices: NumberArrayLike, offset: number, stride: number) {
- if (this.sequence) this.sequence.apply(slot.applied, this);
-
- const bone = slot.bone.applied;
- const vertexOffset = this.offset;
- const x = bone.worldX, y = bone.worldY;
- const a = bone.a, b = bone.b, c = bone.c, d = bone.d;
- let offsetX = 0, offsetY = 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;
- }
-
- copy (): Attachment {
- const copy = new RegionAttachment(this.name, this.path);
- copy.region = this.region;
- copy.x = this.x;
- copy.y = this.y;
- copy.scaleX = this.scaleX;
- copy.scaleY = this.scaleY;
- copy.rotation = this.rotation;
- copy.width = this.width;
- copy.height = this.height;
- 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 != null ? this.sequence.copy() : null;
- return copy;
- }
-
static X1 = 0;
static Y1 = 1;
static C1R = 2;
diff --git a/spine-ts/spine-core/src/attachments/Sequence.ts b/spine-ts/spine-core/src/attachments/Sequence.ts
index b7605eb38..81fd4c49f 100644
--- a/spine-ts/spine-core/src/attachments/Sequence.ts
+++ b/spine-ts/spine-core/src/attachments/Sequence.ts
@@ -29,45 +29,103 @@
import type { SlotPose } from "src/SlotPose.js";
import type { TextureRegion } from "../Texture.js";
-import { Utils } from "../Utils.js";
-import type { HasTextureRegion } from "./HasTextureRegion.js";
-
+import { type NumberArrayLike, Utils } from "../Utils.js";
+import type { HasSequence } from "./HasSequence.js";
+import { MeshAttachment } from "./MeshAttachment.js";
+import { RegionAttachment } from "./RegionAttachment.js";
+/** Holds texture regions, UVs, and vertex offsets for rendering a region or mesh attachment. {@link #getRegions() Regions} must
+ * be populated and {@link #update(HasSequence)} called before use. */
export class Sequence {
private static _nextID = 0;
id = Sequence.nextID();
regions: Array;
+ readonly pathSuffix: boolean;
+ uvs?: NumberArrayLike[];
+
+ /** Returns vertex offsets from the center of a {@link RegionAttachment}. Invalid to call for a {@link MeshAttachment}. */
+ offsets?: number[][];
+
start = 0;
digits = 0;
/** The index of the region to show for the setup pose. */
setupIndex = 0;
- constructor (count: number) {
+ constructor (count: number, pathSuffix: boolean) {
this.regions = new Array(count);
+ this.pathSuffix = pathSuffix;
}
copy (): Sequence {
- const copy = new Sequence(this.regions.length);
- Utils.arrayCopy(this.regions, 0, copy.regions, 0, this.regions.length);
+ const regionCount = this.regions.length;
+ const copy = new Sequence(regionCount, this.pathSuffix);
+ Utils.arrayCopy(this.regions, 0, copy.regions, 0, regionCount);
copy.start = this.start;
copy.digits = this.digits;
copy.setupIndex = this.setupIndex;
+
+ if (this.uvs != null) {
+ const length = this.uvs[0].length;
+ copy.uvs = [];
+ for (let i = 0; i < regionCount; i++) {
+ copy.uvs[i] = Utils.newFloatArray(length);
+ Utils.arrayCopy(this.uvs[i], 0, copy.uvs[i], 0, length);
+ }
+ }
+ if (this.offsets != null) {
+ copy.offsets = [];
+ for (let i = 0; i < regionCount; i++) {
+ copy.offsets[i] = [];
+ Utils.arrayCopy(this.offsets[i], 0, copy.offsets[i], 0, 8);
+ }
+ }
+
return copy;
}
- apply (slot: SlotPose, attachment: HasTextureRegion) {
- let index = slot.sequenceIndex;
- if (index === -1) index = this.setupIndex;
- if (index >= this.regions.length) index = this.regions.length - 1;
- const 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 update (attachment: HasSequence) {
+ const regionCount = this.regions.length;
+ if (attachment instanceof RegionAttachment) {
+ this.uvs = [];
+ this.offsets = [];
+ for (let i = 0; i < regionCount; i++) {
+ this.uvs[i] = Utils.newFloatArray(8);
+ this.offsets[i] = [];
+ RegionAttachment.computeUVs(this.regions[i], attachment.x, attachment.y, attachment.scaleX, attachment.scaleY, attachment.rotation,
+ attachment.width, attachment.height, this.offsets[i], this.uvs[i]);
+ }
+ } else if (attachment instanceof MeshAttachment) {
+ const regionUVs = attachment.regionUVs;
+ this.uvs = [];
+ this.offsets = undefined;
+ for (let i = 0; i < regionCount; i++) {
+ this.uvs[i] = Utils.newFloatArray(regionUVs.length);
+ MeshAttachment.computeUVs(this.regions[i], regionUVs, this.uvs[i]);
+ }
}
}
+ resolveIndex (pose: SlotPose): number {
+ let index = pose.sequenceIndex;
+ if (index === -1) index = this.setupIndex;
+ if (index >= this.regions.length) index = this.regions.length - 1;
+ return index;
+ }
+
+ getUVs (index: number): Float32Array {
+ // biome-ignore lint/style/noNonNullAssertion: uvs are always defined after updateSequence
+ return this.uvs![index] as Float32Array;
+ }
+
+ public hasPathSuffix (): boolean {
+ return this.pathSuffix;
+ }
+
getPath (basePath: string, index: number): string {
+ if (!this.pathSuffix) return basePath;
let result = basePath;
const frame = (this.start + index).toString();
for (let i = this.digits - frame.length; i > 0; i--)
diff --git a/spine-ts/spine-core/src/index.ts b/spine-ts/spine-core/src/index.ts
index 454f2de5b..155f8a0bb 100644
--- a/spine-ts/spine-core/src/index.ts
+++ b/spine-ts/spine-core/src/index.ts
@@ -7,7 +7,7 @@ export * from './attachments/Attachment.js';
export * from './attachments/AttachmentLoader.js';
export * from './attachments/BoundingBoxAttachment.js';
export * from './attachments/ClippingAttachment.js';
-export * from './attachments/HasTextureRegion.js';
+export * from './attachments/HasSequence.js';
export * from './attachments/MeshAttachment.js';
export * from './attachments/PathAttachment.js';
export * from './attachments/PointAttachment.js';
diff --git a/spine-ts/spine-pixi-v7/example/dragon.html b/spine-ts/spine-pixi-v7/example/dragon.html
new file mode 100644
index 000000000..9adde57c2
--- /dev/null
+++ b/spine-ts/spine-pixi-v7/example/dragon.html
@@ -0,0 +1,74 @@
+
+
+
+ spine-pixi-v7
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spine-ts/spine-pixi-v7/src/Spine.ts b/spine-ts/spine-pixi-v7/src/Spine.ts
index c25b8799d..f624abed0 100644
--- a/spine-ts/spine-pixi-v7/src/Spine.ts
+++ b/spine-ts/spine-pixi-v7/src/Spine.ts
@@ -568,7 +568,7 @@ export class Spine extends Container {
slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue;
if (slotObject.visible) {
- let applied = slot.bone.applied;
+ const applied = slot.bone.applied;
const matrix = slotObject.localTransform;
matrix.a = applied.a;
@@ -659,10 +659,14 @@ export class Spine extends Container {
const region = attachment;
attachmentColor = region.color;
numFloats = vertexSize * 4;
- region.computeWorldVertices(slot, this.verticesCache, 0, vertexSize);
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(pose), this.verticesCache, 0, vertexSize);
+
triangles = Spine.QUAD_TRIANGLES;
- uvs = region.uvs;
- texture = region.region?.texture;
+ uvs = sequence.getUVs(sequenceIndex);
+ texture = sequence.regions[sequenceIndex]?.texture as SpineTexture;
} else if (attachment instanceof MeshAttachment) {
const mesh = attachment;
attachmentColor = mesh.color;
@@ -672,8 +676,12 @@ export class Spine extends Container {
}
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize);
triangles = mesh.triangles;
- uvs = mesh.uvs;
- texture = mesh.region?.texture;
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+
+ uvs = sequence.getUVs(sequenceIndex);
+ texture = sequence.regions[sequenceIndex]?.texture as SpineTexture;
} else if (attachment instanceof ClippingAttachment) {
Spine.clipper.clipStart(skeleton, slot, attachment);
pixiMaskSource = { slot, computed: false };
diff --git a/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts b/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts
index d1deb0bb9..c31ce81f9 100644
--- a/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts
+++ b/spine-ts/spine-pixi-v7/src/SpineDebugRenderer.ts
@@ -345,11 +345,9 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
continue;
}
- const regionAttachment = attachment;
-
const vertices = new Float32Array(8);
- regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
debugDisplayObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8)));
}
}
diff --git a/spine-ts/spine-pixi-v8/src/Spine.ts b/spine-ts/spine-pixi-v8/src/Spine.ts
index 3fbf7d0dd..4be7acde5 100644
--- a/spine-ts/spine-pixi-v8/src/Spine.ts
+++ b/spine-ts/spine-pixi-v8/src/Spine.ts
@@ -58,7 +58,6 @@ import {
Container,
type ContainerOptions,
type DestroyOptions,
- fastCopy,
Graphics,
type PointData,
Texture,
@@ -66,6 +65,7 @@ import {
ViewContainer,
} from 'pixi.js';
import type { ISpineDebugRenderer } from './SpineDebugRenderer.js';
+import type { SpineTexture } from './SpineTexture.js';
/**
* Options to create a {@link Spine} using {@link Spine.from}.
@@ -662,8 +662,11 @@ export class Spine extends ViewContainer {
if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) {
const cacheData = this._getCachedData(slot, attachment);
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+
if (attachment instanceof RegionAttachment) {
- attachment.computeWorldVertices(slot, cacheData.vertices, 0, 2);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(pose), cacheData.vertices, 0, 2);
}
else {
attachment.computeWorldVertices(
@@ -677,13 +680,7 @@ export class Spine extends ViewContainer {
);
}
- // sequences uvs are known only after computeWorldVertices is invoked
- if (cacheData.uvs.length < attachment.uvs.length) {
- cacheData.uvs = new Float32Array(attachment.uvs.length);
- }
-
- // need to copy because attachments uvs are shared among skeletons using the same atlas
- fastCopy((attachment.uvs as Float32Array).buffer, cacheData.uvs.buffer);
+ cacheData.uvs = sequence.getUVs(sequenceIndex);
const skeletonColor = skeleton.color;
const slotColor = pose.color;
@@ -708,7 +705,7 @@ export class Spine extends ViewContainer {
cacheData.darkColor.setFromColor(pose.darkColor);
}
- const texture = attachment.region?.texture.texture || Texture.EMPTY;
+ const texture = (sequence.regions[sequenceIndex]?.texture as SpineTexture)?.texture || Texture.EMPTY;
if (cacheData.texture !== texture) {
cacheData.texture = texture;
@@ -834,33 +831,35 @@ export class Spine extends ViewContainer {
if (attachment instanceof RegionAttachment) {
vertices = new Float32Array(8);
+ const sequence = attachment.sequence;
this.attachmentCacheData[slot.data.index][attachment.name] = {
id: `${slot.data.index}-${attachment.name}`,
vertices,
clipped: false,
indices: [0, 1, 2, 0, 2, 3],
- uvs: new Float32Array(attachment.uvs.length),
+ uvs: new Float32Array(sequence.getUVs(0).length),
color: new Color(1, 1, 1, 1),
darkColor: new Color(0, 0, 0, 0),
darkTint: this.darkTint,
skipRender: false,
- texture: attachment.region?.texture.texture,
+ texture: (sequence.regions[0]?.texture as SpineTexture)?.texture,
};
}
else {
vertices = new Float32Array(attachment.worldVerticesLength);
+ const sequence = attachment.sequence;
this.attachmentCacheData[slot.data.index][attachment.name] = {
id: `${slot.data.index}-${attachment.name}`,
vertices,
clipped: false,
indices: attachment.triangles,
- uvs: new Float32Array(attachment.uvs.length),
+ uvs: new Float32Array(sequence.getUVs(0).length),
color: new Color(1, 1, 1, 1),
darkColor: new Color(0, 0, 0, 0),
darkTint: this.darkTint,
skipRender: false,
- texture: attachment.region?.texture.texture,
+ texture: (sequence.regions[0]?.texture as SpineTexture)?.texture,
};
}
diff --git a/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts b/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts
index a8a545f8c..7f00b85ee 100644
--- a/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts
+++ b/spine-ts/spine-pixi-v8/src/SpineDebugRenderer.ts
@@ -366,12 +366,9 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
continue;
}
- const regionAttachment = attachment;
-
const vertices = new Float32Array(8);
- regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
-
+ attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
debugDisplayObjects.regionAttachmentsShape.poly(Array.from(vertices.slice(0, 8)));
}
diff --git a/spine-ts/spine-pixi-v8/src/SpinePipe.ts b/spine-ts/spine-pixi-v8/src/SpinePipe.ts
index c9e878f5e..31c79da22 100644
--- a/spine-ts/spine-pixi-v8/src/SpinePipe.ts
+++ b/spine-ts/spine-pixi-v8/src/SpinePipe.ts
@@ -216,8 +216,10 @@ export class SpinePipe implements RenderPipe {
if (!cacheData.skipRender) {
const batchableSpineSlot = gpuSpine.slotBatches[cacheData.id];
- // we didn't figure out why batchableSpineSlot might be undefined: https://github.com/EsotericSoftware/spine-runtimes/issues/2991
- batchableSpineSlot?._batcher?.updateElement(batchableSpineSlot);
+ if (batchableSpineSlot) {
+ batchableSpineSlot.uvs = cacheData.uvs;
+ batchableSpineSlot._batcher?.updateElement(batchableSpineSlot);
+ }
}
}
}
diff --git a/spine-ts/spine-threejs/src/SkeletonMesh.ts b/spine-ts/spine-threejs/src/SkeletonMesh.ts
index abbfa8ec9..4ba6b4d6b 100644
--- a/spine-ts/spine-threejs/src/SkeletonMesh.ts
+++ b/spine-ts/spine-threejs/src/SkeletonMesh.ts
@@ -246,10 +246,14 @@ export class SkeletonMesh extends THREE.Object3D {
attachmentColor = attachment.color;
vertices = this.vertices;
numFloats = vertexSize * 4;
- attachment.computeWorldVertices(slot, vertices, 0, vertexSize);
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(pose), vertices, 0, vertexSize);
+
triangles = SkeletonMesh.QUAD_TRIANGLES;
- uvs = attachment.uvs;
- texture = attachment.region?.texture;
+ uvs = sequence.getUVs(sequenceIndex);
+ texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof MeshAttachment) {
attachmentColor = attachment.color;
vertices = this.vertices;
@@ -267,8 +271,12 @@ export class SkeletonMesh extends THREE.Object3D {
vertexSize
);
triangles = attachment.triangles;
- uvs = attachment.uvs;
- texture = attachment.region?.texture;
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+
+ uvs = sequence.getUVs(sequenceIndex);
+ texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof ClippingAttachment) {
clipper.clipEnd(slot);
clipper.clipStart(skeleton, slot, attachment);
diff --git a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts
index 5001c8c19..1d9c4800d 100644
--- a/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts
+++ b/spine-ts/spine-webcomponents/src/SpineWebComponentSkeleton.ts
@@ -1160,12 +1160,10 @@ export class SpineWebComponentSkeleton extends HTMLElement implements Disposable
// we could probably cache the vertices from rendering if interaction with this slot is enabled
if (attachment instanceof RegionAttachment) {
- const regionAttachment = attachment;
- regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
} else if (attachment instanceof MeshAttachment) {
- const mesh = attachment;
- mesh.computeWorldVertices(this.skeleton as Skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, 2);
- hullLength = mesh.hullLength;
+ attachment.computeWorldVertices(this.skeleton as Skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
+ hullLength = attachment.hullLength;
}
// here we have only "move" and "drag" events
diff --git a/spine-ts/spine-webgl/example/barebones-dragon.html b/spine-ts/spine-webgl/example/barebones-dragon.html
deleted file mode 100644
index ab16c34c8..000000000
--- a/spine-ts/spine-webgl/example/barebones-dragon.html
+++ /dev/null
@@ -1,86 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/spine-ts/spine-webgl/example/dragon.html b/spine-ts/spine-webgl/example/dragon.html
new file mode 100644
index 000000000..d1ccfcf90
--- /dev/null
+++ b/spine-ts/spine-webgl/example/dragon.html
@@ -0,0 +1,122 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts b/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts
index e92d3c1b4..8149fd576 100644
--- a/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts
+++ b/spine-ts/spine-webgl/src/SkeletonDebugRenderer.ts
@@ -91,7 +91,8 @@ export class SkeletonDebugRenderer implements Disposable {
const attachment = slot.applied.attachment;
if (attachment instanceof RegionAttachment) {
const vertices = this.vertices;
- attachment.computeWorldVertices(slot, vertices, 0, 2);
+
+ attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), 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]);
diff --git a/spine-ts/spine-webgl/src/SkeletonRenderer.ts b/spine-ts/spine-webgl/src/SkeletonRenderer.ts
index 7b7e68ef0..0cadc213e 100644
--- a/spine-ts/spine-webgl/src/SkeletonRenderer.ts
+++ b/spine-ts/spine-webgl/src/SkeletonRenderer.ts
@@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
-import { type BlendMode, ClippingAttachment, Color, MeshAttachment, type NumberArrayLike, RegionAttachment, type Skeleton, SkeletonClipping, type TextureRegion, Utils, Vector2 } from "@esotericsoftware/spine-core";
+import { type BlendMode, ClippingAttachment, Color, MeshAttachment, type NumberArrayLike, RegionAttachment, type Skeleton, SkeletonClipping, Utils, Vector2 } from "@esotericsoftware/spine-core";
import type { GLTexture } from "./GLTexture.js";
import type { PolygonBatcher } from "./PolygonBatcher.js";
import type { ManagedWebGLRenderingContext } from "./WebGL.js";
@@ -102,10 +102,14 @@ export class SkeletonRenderer {
renderable.vertices = this.vertices;
renderable.numVertices = 4;
renderable.numFloats = vertexSize << 2;
- attachment.computeWorldVertices(slot, renderable.vertices, 0, vertexSize);
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+ attachment.computeWorldVertices(slot, attachment.getOffsets(pose), renderable.vertices, 0, vertexSize);
+
triangles = SkeletonRenderer.QUAD_TRIANGLES;
- uvs = attachment.uvs;
- texture = (attachment.region as TextureRegion).texture as GLTexture;
+ uvs = sequence.getUVs(sequenceIndex);
+ texture = sequence.regions[sequenceIndex]?.texture as GLTexture;
attachmentColor = attachment.color;
} else if (attachment instanceof MeshAttachment) {
renderable.vertices = this.vertices;
@@ -117,8 +121,12 @@ export class SkeletonRenderer {
}
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize);
triangles = attachment.triangles;
- texture = (attachment.region as TextureRegion).texture as GLTexture;
- uvs = attachment.uvs;
+
+ const sequence = attachment.sequence;
+ const sequenceIndex = sequence.resolveIndex(pose);
+
+ texture = sequence.regions[sequenceIndex]?.texture as GLTexture;
+ uvs = sequence.getUVs(sequenceIndex);
attachmentColor = attachment.color;
} else if (attachment instanceof ClippingAttachment) {
clipper.clipEnd(slot);