[ts] Port latest libgdx timeline, sequence, and follow-up fixes.

This commit is contained in:
Davide Tantillo 2026-03-15 10:30:16 +01:00
parent cf45806bdd
commit a5ac6e4dd5
29 changed files with 908 additions and 730 deletions

View File

@ -47,6 +47,7 @@
<li><a href="/spine-pixi-v8/example/bounds.html">Bounds</a> - (<a href="/spine-pixi-v7/example/bounds.html">v7</a>)</li> <li><a href="/spine-pixi-v8/example/bounds.html">Bounds</a> - (<a href="/spine-pixi-v7/example/bounds.html">v7</a>)</li>
<li><a href="/spine-pixi-v8/example/inline-loading.html">Inline loading</a> - (<a href="/spine-pixi-v7/example/inline-loading.html">v7</a>)</li> <li><a href="/spine-pixi-v8/example/inline-loading.html">Inline loading</a> - (<a href="/spine-pixi-v7/example/inline-loading.html">v7</a>)</li>
<li><a href="/spine-pixi-v8/example/bunnymark.html?count=500&renderer=webgpu">Bunny Mark</a> - (<a href="/spine-pixi-v7/example/bunnymark.html?count=500">v7</a>)</li> <li><a href="/spine-pixi-v8/example/bunnymark.html?count=500&renderer=webgpu">Bunny Mark</a> - (<a href="/spine-pixi-v7/example/bunnymark.html?count=500">v7</a>)</li>
<li><a href="/spine-pixi-v8/example/dragon.html">Dragon</a> - (<a href="/spine-pixi-v7/example/dragon.html">v7</a>)</li>
</ul> </ul>
<li>Phaser</li> <li>Phaser</li>
<ul> <ul>
@ -142,6 +143,7 @@
<ul> <ul>
<li><a href="/spine-webgl/example">Example</a></li> <li><a href="/spine-webgl/example">Example</a></li>
<li><a href="/spine-webgl/example/barebones.html">Barebones</a></li> <li><a href="/spine-webgl/example/barebones.html">Barebones</a></li>
<li><a href="/spine-webgl/example/dragon.html">Dragon</a></li>
<li><a href="/spine-webgl/example/physics.html">Physics</a></li> <li><a href="/spine-webgl/example/physics.html">Physics</a></li>
<li><a href="/spine-webgl/example/physics2.html">Physics II</a></li> <li><a href="/spine-webgl/example/physics2.html">Physics II</a></li>
<li><a href="/spine-webgl/example/physics3.html">Physics III</a></li> <li><a href="/spine-webgl/example/physics3.html">Physics III</a></li>

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/ *****************************************************************************/
import { type BlendMode, Color, MeshAttachment, RegionAttachment, type Skeleton, type Slot, type TextureRegion, Utils } from "@esotericsoftware/spine-core"; import { type BlendMode, Color, MeshAttachment, type NumberArrayLike, RegionAttachment, type Skeleton, type Slot, type TextureRegion, Utils } from "@esotericsoftware/spine-core";
import type { CanvasTexture } from "./CanvasTexture.js"; import type { CanvasTexture } from "./CanvasTexture.js";
const worldVertices = Utils.newFloatArray(8); const worldVertices = Utils.newFloatArray(8);
@ -68,10 +68,14 @@ export class SkeletonRenderer {
const pose = slot.applied; const pose = slot.applied;
const attachment = pose.attachment; const attachment = pose.attachment;
if (!(attachment instanceof RegionAttachment)) continue; if (!(attachment instanceof RegionAttachment)) continue;
attachment.computeWorldVertices(slot, worldVertices, 0, 2);
const region: TextureRegion = <TextureRegion>attachment.region;
const image: HTMLImageElement = (<CanvasTexture>region.texture).getImage() as HTMLImageElement; const sequence = attachment.sequence;
const sequenceIndex = sequence.resolveIndex(pose);
attachment.computeWorldVertices(slot, attachment.getOffsets(pose), worldVertices, 0, 2);
const region = sequence.regions[sequenceIndex] as TextureRegion;
const image: HTMLImageElement = region.texture.getImage() as HTMLImageElement;
const slotColor = pose.color; const slotColor = pose.color;
const regionColor = attachment.color; const regionColor = attachment.color;
@ -83,7 +87,8 @@ export class SkeletonRenderer {
ctx.save(); ctx.save();
const boneApplied = bone.applied; const boneApplied = bone.applied;
ctx.transform(boneApplied.a, boneApplied.c, boneApplied.b, boneApplied.d, boneApplied.worldX, boneApplied.worldY); ctx.transform(boneApplied.a, boneApplied.c, boneApplied.b, boneApplied.d, boneApplied.worldX, boneApplied.worldY);
ctx.translate(attachment.offset[0], attachment.offset[1]); const offsets = attachment.getOffsets(pose);
ctx.translate(offsets[0], offsets[1]);
ctx.rotate(attachment.rotation * Math.PI / 180); ctx.rotate(attachment.rotation * Math.PI / 180);
const atlasScale = attachment.width / region.originalWidth; const atlasScale = attachment.width / region.originalWidth;
@ -91,7 +96,7 @@ export class SkeletonRenderer {
let w = region.width, h = region.height; let w = region.width, h = region.height;
ctx.translate(w / 2, h / 2); ctx.translate(w / 2, h / 2);
if (attachment.region?.degrees === 90) { if (region.degrees === 90) {
const t = w; const t = w;
w = h; w = h;
h = t; h = t;
@ -124,15 +129,25 @@ export class SkeletonRenderer {
let texture: HTMLImageElement; let texture: HTMLImageElement;
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
const regionAttachment = <RegionAttachment>attachment; const sequence = attachment.sequence;
vertices = this.computeRegionVertices(slot, regionAttachment, false); const sequenceIndex = sequence.resolveIndex(pose);
const uvs = sequence.getUVs(sequenceIndex);
const offsets = attachment.getOffsets(pose);
vertices = this.computeRegionVertices(slot, attachment, offsets, uvs, false);
triangles = SkeletonRenderer.QUAD_TRIANGLES; triangles = SkeletonRenderer.QUAD_TRIANGLES;
texture = (<CanvasTexture>regionAttachment.region?.texture).getImage() as HTMLImageElement;
texture = (sequence.regions[sequenceIndex]?.texture as CanvasTexture).getImage();
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
const mesh = <MeshAttachment>attachment; const sequence = attachment.sequence;
vertices = this.computeMeshVertices(slot, mesh, false); const sequenceIndex = sequence.resolveIndex(pose);
triangles = mesh.triangles;
texture = (<CanvasTexture>mesh.region?.texture).getImage() as HTMLImageElement; const uvs = sequence.getUVs(sequenceIndex);
vertices = this.computeMeshVertices(slot, attachment, uvs, false);
triangles = attachment.triangles;
texture = (sequence.regions[sequenceIndex]?.texture as CanvasTexture).getImage();
} else } else
continue; continue;
@ -226,7 +241,7 @@ export class SkeletonRenderer {
ctx.restore(); 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 skeletonColor = slot.skeleton.color;
const slotColor = slot.applied.color; const slotColor = slot.applied.color;
const regionColor = region.color; const regionColor = region.color;
@ -238,10 +253,9 @@ export class SkeletonRenderer {
skeletonColor.b * slotColor.b * regionColor.b * multiplier, skeletonColor.b * slotColor.b * regionColor.b * multiplier,
alpha); 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 vertices = this.vertices;
const uvs = region.uvs;
vertices[RegionAttachment.C1R] = color.r; vertices[RegionAttachment.C1R] = color.r;
vertices[RegionAttachment.C1G] = color.g; vertices[RegionAttachment.C1G] = color.g;
@ -274,7 +288,7 @@ export class SkeletonRenderer {
return vertices; 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 skeleton = slot.skeleton;
const skeletonColor = skeleton.color; const skeletonColor = skeleton.color;
const slotColor = slot.applied.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); if (vertices.length < mesh.worldVerticesLength) this.vertices = vertices = Utils.newFloatArray(mesh.worldVerticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE); 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++) { for (let i = 0, u = 0, v = 2; i < vertexCount; i++) {
vertices[v++] = color.r; vertices[v++] = color.r;
vertices[v++] = color.g; vertices[v++] = color.g;

View File

@ -28,7 +28,7 @@
*****************************************************************************/ *****************************************************************************/
import { type Attachment, VertexAttachment } from "./attachments/Attachment.js"; 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 { SequenceMode, SequenceModeValues } from "./attachments/Sequence.js";
import type { Inherit } from "./BoneData.js"; import type { Inherit } from "./BoneData.js";
import type { BoneLocal } from "./BoneLocal.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) { protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
const frames = this.frames;
const color = pose.color; const color = pose.color;
const frames = this.frames;
if (time < frames[0]) { if (time < frames[0]) {
const setup = slot.data.setup.color; const setup = slot.data.setup.color;
switch (blend) { switch (blend) {
@ -977,8 +977,12 @@ export class RGBATimeline extends SlotCurveTimeline {
if (alpha === 1) if (alpha === 1)
color.set(r, g, b, a); color.set(r, g, b, a);
else { else {
if (blend === MixBlend.setup) color.setFromColor(slot.data.setup.color); if (blend === MixBlend.setup) {
color.add((r - color.r) * alpha, (g - color.g) * alpha, (b - color.b) * alpha, (a - color.a) * alpha); 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) { protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
const frames = this.frames;
const color = pose.color; const color = pose.color;
let r = 0, g = 0, b = 0;
const frames = this.frames;
if (time < frames[0]) { if (time < frames[0]) {
const setup = slot.data.setup.color; const setup = slot.data.setup.color;
switch (blend) { switch (blend) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.setup: case MixBlend.setup:
color.r = setup.r; color.r = setup.r;
color.g = setup.g; color.g = setup.g;
color.b = setup.b; color.b = setup.b;
// Fall through.
// biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
default:
return; return;
case MixBlend.first: case MixBlend.first:
color.r += (setup.r - color.r) * alpha; r = color.r + (setup.r - color.r) * alpha;
color.g += (setup.g - color.g) * alpha; g = color.g + (setup.g - color.g) * alpha;
color.b += (setup.b - color.b) * 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 { } else {
if (blend === MixBlend.setup) { const i = Timeline.search(frames, time, 4/*ENTRIES*/);
const setup = slot.data.setup.color; const curveType = this.curves[i >> 2];
color.r = setup.r; switch (curveType) {
color.g = setup.g; case 0/*LINEAR*/: {
color.b = setup.b; 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; if (!slot.bone.active) return;
const color = (appliedPose ? slot.applied : slot.pose).color; const color = (appliedPose ? slot.applied : slot.pose).color;
let a = 0;
const frames = this.frames; const frames = this.frames;
if (time < frames[0]) { if (time < frames[0]) {
const setup = slot.data.setup.color; const setup = slot.data.setup.color;
switch (blend) { switch (blend) {
case MixBlend.setup: color.a = setup.a; break; // biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.first: color.a += (setup.a - color.a) * alpha; break; case MixBlend.setup:
} color.a = setup.a;
return; // 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) } else {
color.a = a; a = this.getCurveValue(time);
else { if (alpha !== 1) {
if (blend === MixBlend.setup) color.a = slot.data.setup.color.a; if (blend === MixBlend.setup) {
color.a += (a - color.a) * alpha; 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) { protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
const frames = this.frames; // biome-ignore lint/style/noNonNullAssertion: reference runtime
// biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
const light = pose.color, dark = pose.darkColor!; const light = pose.color, dark = pose.darkColor!;
let r2 = 0, g2 = 0, b2 = 0
const frames = this.frames;
if (time < frames[0]) { if (time < frames[0]) {
const setup = slot.data.setup; 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!; const setupLight = setup.color, setupDark = setup.darkColor!;
switch (blend) { switch (blend) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.setup: case MixBlend.setup:
light.setFromColor(setupLight); light.setFromColor(setupLight);
dark.r = setupDark.r; dark.r = setupDark.r;
dark.g = setupDark.g; dark.g = setupDark.g;
dark.b = setupDark.b; dark.b = setupDark.b;
// Fall through.
// biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
default:
return; return;
case MixBlend.first: case MixBlend.first:
light.add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha, light.add((setupLight.r - light.r) * alpha, (setupLight.g - light.g) * alpha, (setupLight.b - light.b) * alpha,
(setupLight.a - light.a) * alpha); (setupLight.a - light.a) * alpha);
dark.r += (setupDark.r - dark.r) * alpha; r2 = dark.r + (setupDark.r - dark.r) * alpha;
dark.g += (setupDark.g - dark.g) * alpha; g2 = dark.g + (setupDark.g - dark.g) * alpha;
dark.b += (setupDark.b - dark.b) * 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 { } else {
if (blend === MixBlend.setup) { let r = 0, g = 0, b = 0, a = 0;
const setup = slot.data.setup; const i = Timeline.search(frames, time, 8/*ENTRIES*/);
light.setFromColor(setup.color); const curveType = this.curves[i >> 3];
// biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime switch (curveType) {
const setupDark = setup.darkColor!; case 0/*LINEAR*/: {
dark.r = setupDark.r; const before = frames[i];
dark.g = setupDark.g; r = frames[i + 1/*R*/];
dark.b = setupDark.b; 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) { protected apply1 (slot: Slot, pose: SlotPose, time: number, alpha: number, blend: MixBlend) {
const frames = this.frames; // biome-ignore lint/style/noNonNullAssertion: reference runtime
// biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime
const light = pose.color, dark = pose.darkColor!; 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]) { if (time < frames[0]) {
const setup = slot.data.setup; 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!; const setupLight = setup.color, setupDark = setup.darkColor!;
switch (blend) { switch (blend) {
// biome-ignore lint/suspicious/noFallthroughSwitchClause: reference runtime
case MixBlend.setup: case MixBlend.setup:
light.r = setupLight.r; light.r = setupLight.r;
light.g = setupLight.g; light.g = setupLight.g;
@ -1256,82 +1278,84 @@ export class RGB2Timeline extends SlotCurveTimeline {
dark.r = setupDark.r; dark.r = setupDark.r;
dark.g = setupDark.g; dark.g = setupDark.g;
dark.b = setupDark.b; dark.b = setupDark.b;
// Fall through.
// biome-ignore lint/suspicious/useDefaultSwitchClauseLast: reference runtime
default:
return; return;
case MixBlend.first: case MixBlend.first:
light.r += (setupLight.r - light.r) * alpha; r = light.r + (setupLight.r - light.r) * alpha;
light.g += (setupLight.g - light.g) * alpha; g = light.g + (setupLight.g - light.g) * alpha;
light.b += (setupLight.b - light.b) * alpha; b = light.b + (setupLight.b - light.b) * alpha;
dark.r += (setupDark.r - dark.r) * alpha; r2 = dark.r + (setupDark.r - dark.r) * alpha;
dark.g += (setupDark.g - dark.g) * alpha; g2 = dark.g + (setupDark.g - dark.g) * alpha;
dark.b += (setupDark.b - dark.b) * 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 { } else {
if (blend === MixBlend.setup) { const i = Timeline.search(frames, time, 7/*ENTRIES*/);
const setup = slot.data.setup; const curveType = this.curves[i / 7/*ENTRIES*/];
// biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime switch (curveType) {
const setupLight = setup.color, setupDark = setup.darkColor!; case 0/*LINEAR*/: {
light.r = setupLight.r; const before = frames[i];
light.g = setupLight.g; r = frames[i + 1/*R*/];
light.b = setupLight.b; g = frames[i + 2/*G*/];
dark.r = setupDark.r; b = frames[i + 3/*B*/];
dark.g = setupDark.g; r2 = frames[i + 4/*R2*/];
dark.b = setupDark.b; 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; static DELAY = 2;
readonly slotIndex: number; readonly slotIndex: number;
readonly attachment: HasTextureRegion; readonly attachment: HasSequence;
constructor (frameCount: number, slotIndex: number, attachment: HasTextureRegion) { constructor (frameCount: number, slotIndex: number, attachment: HasSequence) {
// biome-ignore lint/style/noNonNullAssertion: expected behavior from reference runtime // biome-ignore lint/style/noNonNullAssertion: reference runtime
super(frameCount, `${Property.sequence}|${slotIndex}|${attachment.sequence!.id}`); super(frameCount, `${Property.sequence}|${slotIndex}|${attachment.sequence!.id}`);
this.slotIndex = slotIndex; this.slotIndex = slotIndex;
this.attachment = attachment; this.attachment = attachment;
@ -1636,6 +1660,9 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
return this.slotIndex; return this.slotIndex;
} }
/** The attachment for which the {@link SlotPose#getSequenceIndex()} will be set.
* <p>
* See {@link VertexAttachment.timelineAttachment}. */
getAttachment () { getAttachment () {
return this.attachment as unknown as Attachment; return this.attachment as unknown as Attachment;
} }
@ -1658,15 +1685,10 @@ export class SequenceTimeline extends Timeline implements SlotTimeline {
if (!slot.bone.active) return; if (!slot.bone.active) return;
const pose = appliedPose ? slot.applied : slot.pose; 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; 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 (!(isHasSequence(slotAttachment)) || slotAttachment.timelineAttachment !== attachment) return;
if (!sequence) return;
if (direction === MixDirection.out) { if (direction === MixDirection.out) {
if (blend === MixBlend.setup) pose.sequenceIndex = -1; 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 modeAndIndex = frames[i + SequenceTimeline.MODE];
const delay = frames[i + SequenceTimeline.DELAY]; 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]; const mode = SequenceModeValues[modeAndIndex & 0xf];
if (mode !== SequenceMode.hold) { if (mode !== SequenceMode.hold) {
index += (((time - before) / delay + 0.00001) | 0); 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*/); softness = this.getBezierValue(time, i, 2/*SOFTNESS*/, curveType + 18/*BEZIER_SIZE*/ - 2/*BEZIER*/);
} }
switch (blend) { if (blend === MixBlend.setup) {
case MixBlend.setup: { const setup = constraint.data.setup;
const setup = constraint.data.setup; pose.mix = setup.mix + (mix - setup.mix) * alpha;
pose.mix = setup.mix + (mix - setup.mix) * alpha; pose.softness = setup.softness + (softness - setup.softness) * alpha;
pose.softness = setup.softness + (softness - setup.softness) * alpha; if (direction === MixDirection.out) {
if (direction === MixDirection.out) { pose.bendDirection = setup.bendDirection;
pose.bendDirection = setup.bendDirection; pose.compress = setup.compress;
pose.compress = setup.compress; pose.stretch = setup.stretch;
pose.stretch = setup.stretch; return;
return;
}
break;
} }
case MixBlend.first: } else {
case MixBlend.replace: pose.mix += (mix - pose.mix) * alpha;
pose.mix += (mix - pose.mix) * alpha; pose.softness += (softness - pose.softness) * alpha;
pose.softness += (softness - pose.softness) * alpha; if (direction === MixDirection.out) return;
if (direction === MixDirection.out) return;
break;
case MixBlend.add:
pose.mix += mix * alpha;
pose.softness += softness * alpha;
if (direction === MixDirection.out) return;
break;
} }
pose.bendDirection = frames[i + 3/*BEND_DIRECTION*/]; pose.bendDirection = frames[i + 3/*BEND_DIRECTION*/];
pose.compress = frames[i + 4/*COMPRESS*/] !== 0; pose.compress = frames[i + 4/*COMPRESS*/] !== 0;
@ -2105,7 +2117,8 @@ export class PathConstraintSpacingTimeline extends ConstraintTimeline1 {
const constraint = skeleton.constraints[this.constraintIndex]; const constraint = skeleton.constraints[this.constraintIndex];
if (constraint.active) { if (constraint.active) {
const pose = appliedPose ? constraint.applied : constraint.pose; 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*/); y = this.getBezierValue(time, i, 3/*Y*/, curveType + 18/*BEZIER_SIZE*/ * 2 - 2/*BEZIER*/);
} }
switch (blend) { if (blend === MixBlend.setup) {
case MixBlend.setup: { const setup = constraint.data.setup;
const setup = constraint.data.setup; pose.mixRotate = setup.mixRotate + (rotate - setup.mixRotate) * alpha;
pose.mixRotate = setup.mixRotate + (rotate - setup.mixRotate) * alpha; pose.mixX = setup.mixX + (x - setup.mixX) * alpha;
pose.mixX = setup.mixX + (x - setup.mixX) * alpha; pose.mixY = setup.mixY + (y - setup.mixY) * alpha;
pose.mixY = setup.mixY + (y - setup.mixY) * alpha; } else {
break; pose.mixRotate += (rotate - pose.mixRotate) * alpha;
} pose.mixX += (x - pose.mixX) * alpha;
case MixBlend.first: pose.mixY += (y - pose.mixY) * alpha;
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;
} }
} }
} }
/** The base class for most {@link PhysicsConstraint} timelines. */ /** The base class for most {@link PhysicsConstraint} timelines. */
export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 { export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
additive = false;
/** @param constraintIndex -1 for all physics constraints in the skeleton. */ /** @param constraintIndex -1 for all physics constraints in the skeleton. */
constructor (frameCount: number, bezierCount: number, constraintIndex: number, property: number) { constructor (frameCount: number, bezierCount: number, constraintIndex: number, property: number) {
super(frameCount, bezierCount, constraintIndex, property); super(frameCount, bezierCount, constraintIndex, property);
@ -2219,6 +2224,7 @@ export abstract class PhysicsConstraintTimeline extends ConstraintTimeline1 {
apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend, apply (skeleton: Skeleton, lastTime: number, time: number, firedEvents: Array<Event>, alpha: number, blend: MixBlend,
direction: MixDirection, appliedPose: boolean) { direction: MixDirection, appliedPose: boolean) {
if (blend === MixBlend.add && !this.additive) blend = MixBlend.replace;
if (this.constraintIndex === -1) { if (this.constraintIndex === -1) {
const value = time >= this.frames[0] ? this.getCurveValue(time) : 0; const value = time >= this.frames[0] ? this.getCurveValue(time) : 0;
const constraints = skeleton.physics; const constraints = skeleton.physics;
@ -2323,6 +2329,7 @@ export class PhysicsConstraintMassTimeline extends PhysicsConstraintTimeline {
export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline { export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
constructor (frameCount: number, bezierCount: number, constraintIndex: number) { constructor (frameCount: number, bezierCount: number, constraintIndex: number) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind); super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintWind);
this.additive = true;
} }
get (pose: PhysicsConstraintPose): number { get (pose: PhysicsConstraintPose): number {
@ -2342,6 +2349,7 @@ export class PhysicsConstraintWindTimeline extends PhysicsConstraintTimeline {
export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline { export class PhysicsConstraintGravityTimeline extends PhysicsConstraintTimeline {
constructor (frameCount: number, bezierCount: number, constraintIndex: number) { constructor (frameCount: number, bezierCount: number, constraintIndex: number) {
super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity); super(frameCount, bezierCount, constraintIndex, Property.physicsConstraintGravity);
this.additive = true;
} }
get (pose: PhysicsConstraintPose): number { get (pose: PhysicsConstraintPose): number {

View File

@ -198,7 +198,7 @@ export class AnimationState {
Utils.webkit602BugfixHelper(alpha, blend); Utils.webkit602BugfixHelper(alpha, blend);
const timeline = timelines[ii]; const timeline = timelines[ii];
if (timeline instanceof AttachmentTimeline) if (timeline instanceof AttachmentTimeline)
this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments); this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, false, attachments);
else else
timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false); timeline.apply(skeleton, animationLast, applyTime, applyEvents, alpha, blend, MixDirection.in, false);
} }
@ -215,7 +215,7 @@ export class AnimationState {
if (!shortestRotation && timeline instanceof RotateTimeline) { if (!shortestRotation && timeline instanceof RotateTimeline) {
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, current.timelinesRotation, ii << 1, firstFrame); this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, current.timelinesRotation, ii << 1, firstFrame);
} else if (timeline instanceof AttachmentTimeline) { } else if (timeline instanceof AttachmentTimeline) {
this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, attachments); this.applyAttachmentTimeline(timeline, skeleton, applyTime, blend, false, attachments);
} else { } else {
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics // This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
Utils.webkit602BugfixHelper(alpha, blend); Utils.webkit602BugfixHelper(alpha, blend);
@ -286,7 +286,6 @@ export class AnimationState {
from.totalAlpha = 0; from.totalAlpha = 0;
for (let i = 0; i < timelineCount; i++) { for (let i = 0; i < timelineCount; i++) {
const timeline = timelines[i]; const timeline = timelines[i];
let direction = MixDirection.out;
let timelineBlend: MixBlend; let timelineBlend: MixBlend;
let alpha = 0; let alpha = 0;
switch (timelineMode[i]) { switch (timelineMode[i]) {
@ -319,8 +318,9 @@ export class AnimationState {
if (!shortestRotation && timeline instanceof RotateTimeline) if (!shortestRotation && timeline instanceof RotateTimeline)
this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, from.timelinesRotation, i << 1, firstFrame); this.applyRotateTimeline(timeline, skeleton, applyTime, alpha, timelineBlend, from.timelinesRotation, i << 1, firstFrame);
else if (timeline instanceof AttachmentTimeline) 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 { else {
let direction = MixDirection.out;
// This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics // This fixes the WebKit 602 specific issue described at https://esotericsoftware.com/forum/d/10109-ios-10-disappearing-graphics
Utils.webkit602BugfixHelper(alpha, blend); Utils.webkit602BugfixHelper(alpha, blend);
if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend === MixBlend.setup) if (drawOrder && timeline instanceof DrawOrderTimeline && timelineBlend === MixBlend.setup)
@ -338,11 +338,13 @@ export class AnimationState {
return mix; 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]; const slot = skeleton.slots[timeline.slotIndex];
if (!slot.bone.active) return; 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) if (blend === MixBlend.setup || blend === MixBlend.first)
this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments); this.setAttachment(skeleton, slot, slot.data.attachmentName, attachments);
} else } else
@ -965,7 +967,8 @@ export class TrackEntry {
* to 1, which overwrites the skeleton's current pose with this animation. * 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 * 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; alpha: number = 0;
/** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be /** Seconds from 0 to the {@link #getMixDuration()} when mixing from the previous animation to this animation. May be

View File

@ -51,40 +51,27 @@ export class AtlasAttachmentLoader implements AttachmentLoader {
this.allowMissingRegions = allowMissingRegions; this.allowMissingRegions = allowMissingRegions;
} }
loadSequence (name: string, basePath: string, sequence: Sequence) { protected findRegions (name: string, basePath: string, sequence: Sequence) {
const regions = sequence.regions; const regions = sequence.regions;
for (let i = 0, n = regions.length; i < n; i++) { for (let i = 0, n = regions.length; i < n; i++)
const path = sequence.getPath(basePath, i); regions[i] = this.findRegion(name, 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})`); 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 { newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence): RegionAttachment {
const attachment = new RegionAttachment(name, path); this.findRegions(name, path, sequence);
if (sequence != null) { return new RegionAttachment(name, sequence);
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;
} }
newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment { newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment {
const attachment = new MeshAttachment(name, path); this.findRegions(name, path, sequence);
if (sequence != null) { return new MeshAttachment(name, sequence);
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;
} }
newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment { newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment {

View File

@ -210,7 +210,7 @@ export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkC
cwx = a * child.x + b * child.y + parent.worldX; cwx = a * child.x + b * child.y + parent.worldX;
cwy = c * child.x + d * child.y + parent.worldY; cwy = c * child.x + d * child.y + parent.worldY;
} }
// biome-ignore lint/style/noNonNullAssertion: reference-runtime // biome-ignore lint/style/noNonNullAssertion: reference runtime
const pp = parent.bone.parent!.applied; const pp = parent.bone.parent!.applied;
a = pp.a; a = pp.a;
b = pp.b; b = pp.b;

View File

@ -462,7 +462,7 @@ export class Skeleton {
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
verticesLength = 8; verticesLength = 8;
vertices = Utils.setArraySize(temp, verticesLength, 0); vertices = Utils.setArraySize(temp, verticesLength, 0);
attachment.computeWorldVertices(slot, vertices, 0, 2); attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
triangles = Skeleton.quadTriangles; triangles = Skeleton.quadTriangles;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
verticesLength = attachment.worldVerticesLength; verticesLength = attachment.worldVerticesLength;

View File

@ -30,7 +30,7 @@
import { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, 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 { AlphaTimeline, Animation, AttachmentTimeline, type BoneTimeline2, type CurveTimeline, 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 { Attachment, VertexAttachment } from "./attachments/Attachment.js";
import type { AttachmentLoader } from "./attachments/AttachmentLoader.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 type { MeshAttachment } from "./attachments/MeshAttachment.js";
import { Sequence, SequenceModeValues } from "./attachments/Sequence.js"; import { Sequence, SequenceModeValues } from "./attachments/Sequence.js";
import { BoneData } from "./BoneData.js"; import { BoneData } from "./BoneData.js";
@ -261,9 +261,9 @@ export class SkeletonBinary {
data.slot = skeletonData.slots[input.readInt(true)]; data.slot = skeletonData.slots[input.readInt(true)];
const flags = input.readByte(); const flags = input.readByte();
data.skinRequired = (flags & 1) !== 0; data.skinRequired = (flags & 1) !== 0;
data.positionMode = (flags >> 1) & 2; data.positionMode = (flags >> 1) & 0b1;
data.spacingMode = (flags >> 2) & 3; data.spacingMode = (flags >> 2) & 0b11;
data.rotateMode = (flags >> 4) & 3; data.rotateMode = (flags >> 4) & 0b11;
if ((flags & 128) !== 0) data.offsetRotation = input.readFloat(); if ((flags & 128) !== 0) data.offsetRotation = input.readFloat();
const setup = data.setup; const setup = data.setup;
setup.position = input.readFloat(); setup.position = input.readFloat();
@ -375,7 +375,7 @@ export class SkeletonBinary {
if (!parent) throw new Error(`Parent mesh not found: ${linkedMesh.parent}`); if (!parent) throw new Error(`Parent mesh not found: ${linkedMesh.parent}`);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? parent as VertexAttachment : linkedMesh.mesh; linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? parent as VertexAttachment : linkedMesh.mesh;
linkedMesh.mesh.setParentMesh(parent as MeshAttachment); linkedMesh.mesh.setParentMesh(parent as MeshAttachment);
if (linkedMesh.mesh.region != null) linkedMesh.mesh.updateRegion(); linkedMesh.mesh.updateSequence();
} }
this.linkedMeshes.length = 0; this.linkedMeshes.length = 0;
@ -465,7 +465,7 @@ export class SkeletonBinary {
case AttachmentType.Region: { case AttachmentType.Region: {
let path = (flags & 16) !== 0 ? input.readStringRef() : null; let path = (flags & 16) !== 0 ? input.readStringRef() : null;
const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff; 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 rotation = (flags & 128) !== 0 ? input.readFloat() : 0;
const x = input.readFloat(); const x = input.readFloat();
const y = input.readFloat(); const y = input.readFloat();
@ -486,8 +486,7 @@ export class SkeletonBinary {
region.width = width * scale; region.width = width * scale;
region.height = height * scale; region.height = height * scale;
Color.rgba8888ToColor(region.color, color); Color.rgba8888ToColor(region.color, color);
region.sequence = sequence; region.updateSequence();
if (region.region != null) region.updateRegion();
return region; return region;
} }
case AttachmentType.BoundingBox: { case AttachmentType.BoundingBox: {
@ -505,7 +504,7 @@ export class SkeletonBinary {
case AttachmentType.Mesh: { case AttachmentType.Mesh: {
let path = (flags & 16) !== 0 ? input.readStringRef() : name; let path = (flags & 16) !== 0 ? input.readStringRef() : name;
const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff; 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 hullLength = input.readInt(true);
const vertices = this.readVertices(input, (flags & 128) !== 0); const vertices = this.readVertices(input, (flags & 128) !== 0);
const uvs = this.readFloatArray(input, vertices.length, 1); const uvs = this.readFloatArray(input, vertices.length, 1);
@ -523,26 +522,25 @@ export class SkeletonBinary {
if (!mesh) return null; if (!mesh) return null;
mesh.path = path; mesh.path = path;
Color.rgba8888ToColor(mesh.color, color); Color.rgba8888ToColor(mesh.color, color);
mesh.hullLength = hullLength << 1;
mesh.bones = vertices.bones; mesh.bones = vertices.bones;
mesh.vertices = vertices.vertices; mesh.vertices = vertices.vertices;
mesh.worldVerticesLength = vertices.length; mesh.worldVerticesLength = vertices.length;
mesh.triangles = triangles;
mesh.regionUVs = uvs; mesh.regionUVs = uvs;
if (mesh.region != null) mesh.updateRegion(); mesh.triangles = triangles;
mesh.hullLength = hullLength << 1;
mesh.sequence = sequence;
if (nonessential) { if (nonessential) {
mesh.edges = edges; mesh.edges = edges;
mesh.width = width * scale; mesh.width = width * scale;
mesh.height = height * scale; mesh.height = height * scale;
} }
mesh.updateSequence();
return mesh; return mesh;
} }
case AttachmentType.LinkedMesh: { case AttachmentType.LinkedMesh: {
const path = (flags & 16) !== 0 ? input.readStringRef() : name; const path = (flags & 16) !== 0 ? input.readStringRef() : name;
if (path == null) throw new Error("Path of linked mesh must not be null"); if (path == null) throw new Error("Path of linked mesh must not be null");
const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff; 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 inheritTimelines = (flags & 128) !== 0;
const skinIndex = input.readInt(true); const skinIndex = input.readInt(true);
const parent = input.readStringRef(); const parent = input.readStringRef();
@ -556,7 +554,6 @@ export class SkeletonBinary {
if (!mesh) return null; if (!mesh) return null;
mesh.path = path; mesh.path = path;
Color.rgba8888ToColor(mesh.color, color); Color.rgba8888ToColor(mesh.color, color);
mesh.sequence = sequence;
if (nonessential) { if (nonessential) {
mesh.width = width * scale; mesh.width = width * scale;
mesh.height = height * scale; mesh.height = height * scale;
@ -616,8 +613,9 @@ export class SkeletonBinary {
} }
} }
private readSequence (input: BinaryInput) { private readSequence (input: BinaryInput, hasPathSuffix: boolean) {
const sequence = new Sequence(input.readInt(true)); if (!hasPathSuffix) return new Sequence(1, false);
const sequence = new Sequence(input.readInt(true), true);
sequence.start = input.readInt(true); sequence.start = input.readInt(true);
sequence.digits = input.readInt(true); sequence.digits = input.readInt(true);
sequence.setupIndex = input.readInt(true); sequence.setupIndex = input.readInt(true);
@ -632,19 +630,20 @@ export class SkeletonBinary {
if (!weighted) if (!weighted)
return new Vertices(null, this.readFloatArray(input, length, scale), length); return new Vertices(null, this.readFloatArray(input, length, scale), length);
const n = input.readInt(true);
const bones: number[] = [];
const weights: number[] = []; const weights: number[] = [];
const bonesArray: number[] = []; for (let b = 0, w = 0; b < n;) {
for (let i = 0; i < vertexCount; i++) {
const boneCount = input.readInt(true); const boneCount = input.readInt(true);
bonesArray.push(boneCount); bones[b++] = boneCount;
for (let ii = 0; ii < boneCount; ii++) { for (let ii = 0; ii < boneCount; ii++, w += 3) {
bonesArray.push(input.readInt(true)); bones[b++] = input.readInt(true);
weights.push(input.readFloat() * scale); weights[w] = input.readFloat() * scale;
weights.push(input.readFloat() * scale); weights[w + 1] = input.readFloat() * scale;
weights.push(input.readFloat()); 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[] { private readFloatArray (input: BinaryInput, n: number, scale: number): number[] {
@ -1115,7 +1114,7 @@ export class SkeletonBinary {
break; break;
} }
case ATTACHMENT_SEQUENCE: { 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++) { for (let frame = 0; frame < frameCount; frame++) {
const time = input.readFloat(); const time = input.readFloat();
const modeAndIndex = input.readInt32(); const modeAndIndex = input.readInt32();

View File

@ -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 { 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 { Attachment, VertexAttachment } from "./attachments/Attachment.js";
import type { AttachmentLoader } from "./attachments/AttachmentLoader.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 type { MeshAttachment } from "./attachments/MeshAttachment.js";
import { Sequence, SequenceMode } from "./attachments/Sequence.js"; import { Sequence, SequenceMode } from "./attachments/Sequence.js";
import { BoneData, Inherit } from "./BoneData.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}`); if (!parent) throw new Error(`Parent mesh not found: ${linkedMesh.parent}`);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? <VertexAttachment>parent : <VertexAttachment>linkedMesh.mesh; linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimeline ? <VertexAttachment>parent : <VertexAttachment>linkedMesh.mesh;
linkedMesh.mesh.setParentMesh(<MeshAttachment>parent); linkedMesh.mesh.setParentMesh(<MeshAttachment>parent);
if (linkedMesh.mesh.region != null) linkedMesh.mesh.updateRegion(); linkedMesh.mesh.updateSequence();
} }
this.linkedMeshes.length = 0; this.linkedMeshes.length = 0;
@ -528,12 +528,11 @@ export class SkeletonJson {
region.rotation = getValue(map, "rotation", 0); region.rotation = getValue(map, "rotation", 0);
region.width = map.width * scale; region.width = map.width * scale;
region.height = map.height * scale; region.height = map.height * scale;
region.sequence = sequence;
const color: string = getValue(map, "color", null); const color: string = getValue(map, "color", null);
if (color) region.color.setFromString(color); if (color) region.color.setFromString(color);
if (region.region != null) region.updateRegion(); region.updateSequence();
return region; return region;
} }
case "boundingbox": { case "boundingbox": {
@ -557,7 +556,6 @@ export class SkeletonJson {
mesh.width = getValue(map, "width", 0) * scale; mesh.width = getValue(map, "width", 0) * scale;
mesh.height = getValue(map, "height", 0) * scale; mesh.height = getValue(map, "height", 0) * scale;
mesh.sequence = sequence;
const parent: string = getValue(map, "parent", null); const parent: string = getValue(map, "parent", null);
if (parent) { if (parent) {
@ -569,10 +567,10 @@ export class SkeletonJson {
this.readVertices(map, mesh, uvs.length); this.readVertices(map, mesh, uvs.length);
mesh.triangles = map.triangles; mesh.triangles = map.triangles;
mesh.regionUVs = uvs; mesh.regionUVs = uvs;
if (mesh.region != null) mesh.updateRegion();
mesh.edges = getValue(map, "edges", null); mesh.edges = getValue(map, "edges", null);
mesh.hullLength = getValue(map, "hull", 0) * 2; mesh.hullLength = getValue(map, "hull", 0) * 2;
mesh.updateSequence();
return mesh; return mesh;
} }
case "path": { case "path": {
@ -623,8 +621,8 @@ export class SkeletonJson {
} }
readSequence (map: object) { readSequence (map: object) {
if (map == null) return null; if (map == null) return new Sequence(1, false);
const sequence = new Sequence(getValue(map, "count", 0)); const sequence = new Sequence(getValue(map, "count", 0), true);
sequence.start = getValue(map, "start", 1); sequence.start = getValue(map, "start", 1);
sequence.digits = getValue(map, "digits", 0); sequence.digits = getValue(map, "digits", 0);
sequence.setupIndex = getValue(map, "setup", 0); sequence.setupIndex = getValue(map, "setup", 0);
@ -1155,7 +1153,7 @@ export class SkeletonJson {
} }
timelines.push(timeline); timelines.push(timeline);
} else if (timelineMapName === "sequence") { } 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; let lastDelay = 0;
for (let frame = 0; frame < timelineMap.length; frame++) { for (let frame = 0; frame < timelineMap.length; frame++) {
const delay = getValue(keyMap, "delay", lastDelay); const delay = getValue(keyMap, "delay", lastDelay);

View File

@ -55,8 +55,8 @@ export class SkeletonRendererCore {
continue; continue;
} }
const slotApplied = slot.applied; const pose = slot.applied;
const slotColor = slotApplied.color; const slotColor = pose.color;
const alpha = slotColor.a; const alpha = slotColor.a;
if ((alpha === 0 || !slot.bone.active) && !(attachment instanceof ClippingAttachment)) { if ((alpha === 0 || !slot.bone.active) && !(attachment instanceof ClippingAttachment)) {
clipper.clipEnd(slot); clipper.clipEnd(slot);
@ -80,13 +80,16 @@ export class SkeletonRendererCore {
continue; 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; vertices = this.worldVertices;
verticesCount = 4; verticesCount = 4;
uvs = attachment.uvs as Float32Array; uvs = sequence.getUVs(sequenceIndex);
indices = this.quadIndices; indices = this.quadIndices;
indicesCount = 6; indicesCount = 6;
texture = attachment.region?.texture; texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
attachmentColor = attachment.color; attachmentColor = attachment.color;
@ -102,10 +105,14 @@ export class SkeletonRendererCore {
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, this.worldVertices, 0, stride); attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, this.worldVertices, 0, stride);
vertices = this.worldVertices; vertices = this.worldVertices;
verticesCount = attachment.worldVerticesLength >> 1; 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; indices = attachment.triangles;
indicesCount = indices.length; indicesCount = indices.length;
texture = attachment.region?.texture; texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof ClippingAttachment) { } else if (attachment instanceof ClippingAttachment) {
clipper.clipStart(skeleton, slot, attachment); clipper.clipStart(skeleton, slot, attachment);
@ -133,8 +140,8 @@ export class SkeletonRendererCore {
} }
darkColor = 0xff000000; darkColor = 0xff000000;
if (slotApplied.darkColor) { if (pose.darkColor) {
const { r, g, b } = slotApplied.darkColor; const { r, g, b } = pose.darkColor;
darkColor = 0xff000000 | darkColor = 0xff000000 |
(Math.floor(r * a) << 16) | (Math.floor(r * a) << 16) |
(Math.floor(g * a) << 8) | (Math.floor(g * a) << 8) |
@ -156,8 +163,8 @@ export class SkeletonRendererCore {
} }
darkColor = 0; darkColor = 0;
if (slotApplied.darkColor) { if (pose.darkColor) {
const { r, g, b } = slotApplied.darkColor; const { r, g, b } = pose.darkColor;
darkColor = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255); darkColor = (Math.floor(r * 255) << 16) | (Math.floor(g * 255) << 8) | Math.floor(b * 255);
} }
} }

View File

@ -35,9 +35,14 @@ import { type NumberArrayLike, Utils } from "../Utils.js";
export abstract class Attachment { export abstract class Attachment {
name: string; 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) { constructor (name: string) {
if (!name) throw new Error("name cannot be null."); if (!name) throw new Error("name cannot be null.");
this.name = name; this.name = name;
this.timelineAttachment = this;
} }
abstract copy (): Attachment; abstract copy (): Attachment;
@ -65,10 +70,6 @@ export abstract class VertexAttachment extends Attachment {
* {@link computeWorldVertices} using the `count` parameter. */ * {@link computeWorldVertices} using the `count` parameter. */
worldVerticesLength = 0; 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) { constructor (name: string) {
super(name); super(name);
} }

View File

@ -42,10 +42,10 @@ import type { Sequence } from "./Sequence.js";
* Runtimes Guide. */ * Runtimes Guide. */
export interface AttachmentLoader { export interface AttachmentLoader {
/** @return May be null to not load an attachment. */ /** @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. */ /** @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. */ /** @return May be null to not load an attachment. */
newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment; newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment;

View File

@ -27,24 +27,20 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * 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 { Color } from "../Utils.js"
import type { Sequence } from "./Sequence.js" import type { Sequence } from "./Sequence.js"
export interface HasTextureRegion { export function isHasSequence (obj: unknown): obj is HasSequence {
/** The name used to find the {@link #region()}. */ return !!obj && typeof obj === "object" && "sequence" in obj && "updateSequence" in obj;
path: string; }
/** The region used to draw the attachment. After setting the region or if the region's properties are changed, export interface HasSequence {
* {@link #updateRegion()} must be called. */ path?: string;
region: TextureRegion | null;
/** 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; color: Color;
sequence: Sequence | null; /** Calls {@link Sequence#update(HasSequence)} on this attachment's sequence. */
updateSequence (): void;
sequence: Sequence;
} }

View File

@ -27,74 +27,123 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * 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 type { TextureRegion } from "../Texture.js";
import { TextureAtlasRegion } from "../TextureAtlas.js"; import { TextureAtlasRegion } from "../TextureAtlas.js";
import { Color, type NumberArrayLike, Utils } from "../Utils.js"; import { Color, type NumberArrayLike, Utils } from "../Utils.js";
import { type Attachment, VertexAttachment } from "./Attachment.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"; 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 /** 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. * 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. */ * See [Mesh attachments](http://esotericsoftware.com/spine-meshes) in the Spine User Guide. */
export class MeshAttachment extends VertexAttachment implements HasTextureRegion { export class MeshAttachment extends VertexAttachment implements HasSequence {
region: TextureRegion | null = null; readonly sequence: Sequence;
/** The name of the texture region for this attachment. */
path: string;
/** The UV pair for each vertex, normalized within the texture region. */ /** The UV pair for each vertex, normalized within the texture region. */
regionUVs: NumberArrayLike = []; 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. */ /** Triplets of vertex indices which describe the mesh's triangulation. */
triangles: Array<number> = []; triangles: Array<number> = [];
/** 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. */ /** The color to tint the mesh. */
color = new Color(1, 1, 1, 1); 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<number> = [];
/** The width of the mesh's image. Available only when nonessential data was exported. */ /** The width of the mesh's image. Available only when nonessential data was exported. */
width: number = 0; width: number = 0;
/** The height of the mesh's image. Available only when nonessential data was exported. */ /** The height of the mesh's image. Available only when nonessential data was exported. */
height: number = 0; 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<number> = [];
private parentMesh: MeshAttachment | null = null;
sequence: Sequence | null = null;
tempColor = new Color(0, 0, 0, 0); tempColor = new Color(0, 0, 0, 0);
constructor (name: string, path: string) { constructor (name: string, sequence: Sequence) {
super(name); 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 copy (): Attachment {
* the {@link #regionUVs} are changed. */ if (this.parentMesh) return this.newLinkedMesh();
updateRegion () {
if (!this.region) throw new Error("Region not set."); const copy = new MeshAttachment(this.name, this.sequence.copy());
const regionUVs = this.regionUVs; copy.path = this.path;
if (!this.uvs || this.uvs.length !== regionUVs.length) this.uvs = Utils.newFloatArray(regionUVs.length); copy.color.setFromColor(this.color);
const uvs = this.uvs;
const n = this.uvs.length; this.copyTo(copy);
let u = this.region.u, v = this.region.v, width = 0, height = 0; copy.regionUVs = [];
if (this.region instanceof TextureAtlasRegion) { Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length);
const region = this.region, page = region.page; 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; const textureWidth = page.width, textureHeight = page.height;
switch (region.degrees) { switch (region.degrees) {
case 90: case 90:
@ -133,12 +182,12 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
width = region.originalWidth / textureWidth; width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight; height = region.originalHeight / textureHeight;
} }
} else if (!this.region) { } else if (!region) {
u = v = 0; u = v = 0;
width = height = 1; width = height = 1;
} else { } else {
width = this.region.u2 - u; width = region.u2 - u;
height = this.region.v2 - v; height = region.v2 - v;
} }
for (let i = 0; i < n; i += 2) { 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; 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;
}
} }

View File

@ -27,17 +27,20 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * 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 { Slot } from "../Slot.js";
import type { TextureRegion } from "../Texture.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 { Attachment } from "./Attachment.js";
import type { HasTextureRegion } from "./HasTextureRegion.js"; import type { HasSequence } from "./HasSequence.js";
import type { Sequence } from "./Sequence.js"; import type { Sequence } from "./Sequence.js";
/** An attachment that displays a textured quadrilateral. /** An attachment that displays a textured quadrilateral.
* *
* See [Region attachments](http://esotericsoftware.com/spine-regions) in the Spine User Guide. */ * 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. */ /** The local x translation. */
x = 0; x = 0;
@ -59,44 +62,95 @@ export class RegionAttachment extends Attachment implements HasTextureRegion {
/** The height of the region attachment in Spine. */ /** The height of the region attachment in Spine. */
height = 0; height = 0;
/** The name of the texture region for this attachment. */
path?: string;
/** The color to tint the region attachment. */ /** The color to tint the region attachment. */
color = new Color(1, 1, 1, 1); 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 <code>x,y</code> 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); tempColor = new Color(1, 1, 1, 1);
constructor (name: string, path: string) { constructor (name: string, sequence: Sequence) {
super(name); super(name);
this.path = path; this.sequence = sequence;
} }
/** Calculates the {@link #offset} using the region settings. Must be called after changing region settings. */ copy (): Attachment {
updateRegion (): void { const copy = new RegionAttachment(this.name, this.sequence.copy());
if (!this.region) throw new Error("Region not set."); copy.path = this.path;
const region = this.region; copy.x = this.x;
const uvs = this.uvs; copy.y = this.y;
const regionScaleX = this.width / this.region.originalWidth * this.scaleX; copy.scaleX = this.scaleX;
const regionScaleY = this.height / this.region.originalHeight * this.scaleY; copy.scaleY = this.scaleY;
const localX = -this.width / 2 * this.scaleX + this.region.offsetX * regionScaleX; copy.rotation = this.rotation;
const localY = -this.height / 2 * this.scaleY + this.region.offsetY * regionScaleY; copy.width = this.width;
const localX2 = localX + this.region.width * regionScaleX; copy.height = this.height;
const localY2 = localY + this.region.height * regionScaleY; copy.color.setFromColor(this.color);
const radians = this.rotation * MathUtils.degRad; return copy;
}
/** Transforms the attachment's four vertices to world coordinates.
* <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 >= <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, 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 cos = Math.cos(radians);
const sin = Math.sin(radians); const sin = Math.sin(radians);
const x = this.x, y = this.y;
const localXCos = localX * cos + x; const localXCos = localX * cos + x;
const localXSin = localX * sin; const localXSin = localX * sin;
const localYCos = localY * cos + y; const localYCos = localY * cos + y;
@ -105,7 +159,6 @@ export class RegionAttachment extends Attachment implements HasTextureRegion {
const localX2Sin = localX2 * sin; const localX2Sin = localX2 * sin;
const localY2Cos = localY2 * cos + y; const localY2Cos = localY2 * cos + y;
const localY2Sin = localY2 * sin; const localY2Sin = localY2 * sin;
const offset = this.offset;
offset[0] = localXCos - localYSin; offset[0] = localXCos - localYSin;
offset[1] = localYCos + localXSin; offset[1] = localYCos + localXSin;
offset[2] = localXCos - localY2Sin; offset[2] = localXCos - localY2Sin;
@ -124,85 +177,25 @@ export class RegionAttachment extends Attachment implements HasTextureRegion {
uvs[5] = 1; uvs[5] = 1;
uvs[6] = 1; uvs[6] = 1;
uvs[7] = 0; 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 { } else {
uvs[0] = region.u;
uvs[1] = region.v2; uvs[1] = region.v2;
uvs[2] = region.u; uvs[2] = region.u;
uvs[3] = region.v;
uvs[4] = region.u2;
uvs[5] = region.v; uvs[5] = region.v;
uvs[6] = region.u2; 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.
* <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 >= <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) 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 X1 = 0;
static Y1 = 1; static Y1 = 1;
static C1R = 2; static C1R = 2;

View File

@ -29,45 +29,103 @@
import type { SlotPose } from "src/SlotPose.js"; import type { SlotPose } from "src/SlotPose.js";
import type { TextureRegion } from "../Texture.js"; import type { TextureRegion } from "../Texture.js";
import { Utils } from "../Utils.js"; import { type NumberArrayLike, Utils } from "../Utils.js";
import type { HasTextureRegion } from "./HasTextureRegion.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 { export class Sequence {
private static _nextID = 0; private static _nextID = 0;
id = Sequence.nextID(); id = Sequence.nextID();
regions: Array<TextureRegion | null>; regions: Array<TextureRegion | null>;
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; start = 0;
digits = 0; digits = 0;
/** The index of the region to show for the setup pose. */ /** The index of the region to show for the setup pose. */
setupIndex = 0; setupIndex = 0;
constructor (count: number) { constructor (count: number, pathSuffix: boolean) {
this.regions = new Array<TextureRegion>(count); this.regions = new Array<TextureRegion>(count);
this.pathSuffix = pathSuffix;
} }
copy (): Sequence { copy (): Sequence {
const copy = new Sequence(this.regions.length); const regionCount = this.regions.length;
Utils.arrayCopy(this.regions, 0, copy.regions, 0, this.regions.length); const copy = new Sequence(regionCount, this.pathSuffix);
Utils.arrayCopy(this.regions, 0, copy.regions, 0, regionCount);
copy.start = this.start; copy.start = this.start;
copy.digits = this.digits; copy.digits = this.digits;
copy.setupIndex = this.setupIndex; 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; return copy;
} }
apply (slot: SlotPose, attachment: HasTextureRegion) { /** Computes UVs and offsets for the specified attachment. Must be called if the regions or attachment properties are
let index = slot.sequenceIndex; * changed. */
if (index === -1) index = this.setupIndex; public update (attachment: HasSequence) {
if (index >= this.regions.length) index = this.regions.length - 1; const regionCount = this.regions.length;
const region = this.regions[index]; if (attachment instanceof RegionAttachment) {
if (attachment.region !== region) { this.uvs = [];
attachment.region = region; this.offsets = [];
attachment.updateRegion(); 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 { getPath (basePath: string, index: number): string {
if (!this.pathSuffix) return basePath;
let result = basePath; let result = basePath;
const frame = (this.start + index).toString(); const frame = (this.start + index).toString();
for (let i = this.digits - frame.length; i > 0; i--) for (let i = this.digits - frame.length; i > 0; i--)

View File

@ -7,7 +7,7 @@ export * from './attachments/Attachment.js';
export * from './attachments/AttachmentLoader.js'; export * from './attachments/AttachmentLoader.js';
export * from './attachments/BoundingBoxAttachment.js'; export * from './attachments/BoundingBoxAttachment.js';
export * from './attachments/ClippingAttachment.js'; export * from './attachments/ClippingAttachment.js';
export * from './attachments/HasTextureRegion.js'; export * from './attachments/HasSequence.js';
export * from './attachments/MeshAttachment.js'; export * from './attachments/MeshAttachment.js';
export * from './attachments/PathAttachment.js'; export * from './attachments/PathAttachment.js';
export * from './attachments/PointAttachment.js'; export * from './attachments/PointAttachment.js';

View File

@ -0,0 +1,74 @@
<html>
<head>
<meta charset="UTF-8" />
<title>spine-pixi-v7</title>
<script src="https://cdn.jsdelivr.net/npm/pixi.js@7.4.2/dist/pixi.min.js"></script>
<script src="../dist/iife/spine-pixi-v7.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.umd.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.min.css" rel="stylesheet">
<link rel="stylesheet" href="../../index.css">
</head>
<body>
<script>
(async function () {
var app = new PIXI.Application({
width: window.innerWidth,
height: window.innerHeight,
resolution: window.devicePixelRatio || 1,
autoDensity: true,
resizeTo: window,
backgroundColor: 0x2c3e50,
hello: true,
});
document.body.appendChild(app.view);
PIXI.Assets.add("spineboyData", "/assets/dragon-ess.skel");
PIXI.Assets.add("spineboyAtlas", "/assets/dragon-pma.atlas");
await PIXI.Assets.load(["spineboyData", "spineboyAtlas"]);
const spineboy = new spine.Spine({skeleton: "spineboyData", atlas: "spineboyAtlas", scale: 0.5 });
const spineboy2 = new spine.Spine({skeleton: "spineboyData", atlas: "spineboyAtlas", scale: 0.5 });
spineboy.autoUpdate = false;
spineboy2.autoUpdate = false;
spineboy.state.data.defaultMix = 0.2;
spineboy.x = window.innerWidth / 2;
spineboy.y = window.innerHeight / 2 - 30;
spineboy2.x = window.innerWidth / 2;
spineboy2.y = window.innerHeight / 2 + 200;
spineboy2.state.setAnimation(0, "flying", true);
spineboy.state.setAnimation(0, "flying", true);
app.stage.addChild(spineboy);
app.stage.addChild(spineboy2);
const myObject = { time: 0, time2: 0 };
let prevValue = myObject.time;
let prevValue2 = myObject.time2;
spineboy.update(prevValue / 10)
spineboy2.update(prevValue2 / 10)
const gui = new lil.GUI({});
gui
.add(myObject, 'time').min(0).max(10).step(0.01)
.name( 'time' )
.onChange(value => {
spineboy.update((value - prevValue) / 10)
prevValue = value;
});
gui
.add(myObject, 'time2').min(0).max(10).step(0.01)
.name( 'time2' )
.onChange(value => {
spineboy2.update((value - prevValue2) / 10)
prevValue2 = value;
});
})();
</script>
</body>
</html>

View File

@ -568,7 +568,7 @@ export class Spine extends Container {
slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue; slotObject.visible = this.skeleton.drawOrder.includes(slot) && followAttachmentValue;
if (slotObject.visible) { if (slotObject.visible) {
let applied = slot.bone.applied; const applied = slot.bone.applied;
const matrix = slotObject.localTransform; const matrix = slotObject.localTransform;
matrix.a = applied.a; matrix.a = applied.a;
@ -659,10 +659,14 @@ export class Spine extends Container {
const region = attachment; const region = attachment;
attachmentColor = region.color; attachmentColor = region.color;
numFloats = vertexSize * 4; 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; triangles = Spine.QUAD_TRIANGLES;
uvs = region.uvs; uvs = sequence.getUVs(sequenceIndex);
texture = <SpineTexture>region.region?.texture; texture = sequence.regions[sequenceIndex]?.texture as SpineTexture;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
const mesh = attachment; const mesh = attachment;
attachmentColor = mesh.color; attachmentColor = mesh.color;
@ -672,8 +676,12 @@ export class Spine extends Container {
} }
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize); mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, this.verticesCache, 0, vertexSize);
triangles = mesh.triangles; triangles = mesh.triangles;
uvs = mesh.uvs;
texture = <SpineTexture>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) { } else if (attachment instanceof ClippingAttachment) {
Spine.clipper.clipStart(skeleton, slot, attachment); Spine.clipper.clipStart(skeleton, slot, attachment);
pixiMaskSource = { slot, computed: false }; pixiMaskSource = { slot, computed: false };

View File

@ -345,11 +345,9 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
continue; continue;
} }
const regionAttachment = attachment;
const vertices = new Float32Array(8); 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))); debugDisplayObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8)));
} }
} }

View File

@ -58,7 +58,6 @@ import {
Container, Container,
type ContainerOptions, type ContainerOptions,
type DestroyOptions, type DestroyOptions,
fastCopy,
Graphics, Graphics,
type PointData, type PointData,
Texture, Texture,
@ -66,6 +65,7 @@ import {
ViewContainer, ViewContainer,
} from 'pixi.js'; } from 'pixi.js';
import type { ISpineDebugRenderer } from './SpineDebugRenderer.js'; import type { ISpineDebugRenderer } from './SpineDebugRenderer.js';
import type { SpineTexture } from './SpineTexture.js';
/** /**
* Options to create a {@link Spine} using {@link Spine.from}. * 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) { if (attachment instanceof MeshAttachment || attachment instanceof RegionAttachment) {
const cacheData = this._getCachedData(slot, attachment); const cacheData = this._getCachedData(slot, attachment);
const sequence = attachment.sequence;
const sequenceIndex = sequence.resolveIndex(pose);
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
attachment.computeWorldVertices(slot, cacheData.vertices, 0, 2); attachment.computeWorldVertices(slot, attachment.getOffsets(pose), cacheData.vertices, 0, 2);
} }
else { else {
attachment.computeWorldVertices( attachment.computeWorldVertices(
@ -677,13 +680,7 @@ export class Spine extends ViewContainer {
); );
} }
// sequences uvs are known only after computeWorldVertices is invoked cacheData.uvs = sequence.getUVs(sequenceIndex);
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);
const skeletonColor = skeleton.color; const skeletonColor = skeleton.color;
const slotColor = pose.color; const slotColor = pose.color;
@ -708,7 +705,7 @@ export class Spine extends ViewContainer {
cacheData.darkColor.setFromColor(pose.darkColor); 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) { if (cacheData.texture !== texture) {
cacheData.texture = texture; cacheData.texture = texture;
@ -834,33 +831,35 @@ export class Spine extends ViewContainer {
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
vertices = new Float32Array(8); vertices = new Float32Array(8);
const sequence = attachment.sequence;
this.attachmentCacheData[slot.data.index][attachment.name] = { this.attachmentCacheData[slot.data.index][attachment.name] = {
id: `${slot.data.index}-${attachment.name}`, id: `${slot.data.index}-${attachment.name}`,
vertices, vertices,
clipped: false, clipped: false,
indices: [0, 1, 2, 0, 2, 3], 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), color: new Color(1, 1, 1, 1),
darkColor: new Color(0, 0, 0, 0), darkColor: new Color(0, 0, 0, 0),
darkTint: this.darkTint, darkTint: this.darkTint,
skipRender: false, skipRender: false,
texture: attachment.region?.texture.texture, texture: (sequence.regions[0]?.texture as SpineTexture)?.texture,
}; };
} }
else { else {
vertices = new Float32Array(attachment.worldVerticesLength); vertices = new Float32Array(attachment.worldVerticesLength);
const sequence = attachment.sequence;
this.attachmentCacheData[slot.data.index][attachment.name] = { this.attachmentCacheData[slot.data.index][attachment.name] = {
id: `${slot.data.index}-${attachment.name}`, id: `${slot.data.index}-${attachment.name}`,
vertices, vertices,
clipped: false, clipped: false,
indices: attachment.triangles, indices: attachment.triangles,
uvs: new Float32Array(attachment.uvs.length), uvs: new Float32Array(sequence.getUVs(0).length),
color: new Color(1, 1, 1, 1), color: new Color(1, 1, 1, 1),
darkColor: new Color(0, 0, 0, 0), darkColor: new Color(0, 0, 0, 0),
darkTint: this.darkTint, darkTint: this.darkTint,
skipRender: false, skipRender: false,
texture: attachment.region?.texture.texture, texture: (sequence.regions[0]?.texture as SpineTexture)?.texture,
}; };
} }

View File

@ -366,12 +366,9 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
continue; continue;
} }
const regionAttachment = attachment;
const vertices = new Float32Array(8); 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))); debugDisplayObjects.regionAttachmentsShape.poly(Array.from(vertices.slice(0, 8)));
} }

View File

@ -216,8 +216,10 @@ export class SpinePipe implements RenderPipe<Spine> {
if (!cacheData.skipRender) { if (!cacheData.skipRender) {
const batchableSpineSlot = gpuSpine.slotBatches[cacheData.id]; const batchableSpineSlot = gpuSpine.slotBatches[cacheData.id];
// we didn't figure out why batchableSpineSlot might be undefined: https://github.com/EsotericSoftware/spine-runtimes/issues/2991 if (batchableSpineSlot) {
batchableSpineSlot?._batcher?.updateElement(batchableSpineSlot); batchableSpineSlot.uvs = cacheData.uvs;
batchableSpineSlot._batcher?.updateElement(batchableSpineSlot);
}
} }
} }
} }

View File

@ -246,10 +246,14 @@ export class SkeletonMesh extends THREE.Object3D {
attachmentColor = attachment.color; attachmentColor = attachment.color;
vertices = this.vertices; vertices = this.vertices;
numFloats = vertexSize * 4; 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; triangles = SkeletonMesh.QUAD_TRIANGLES;
uvs = attachment.uvs; uvs = sequence.getUVs(sequenceIndex);
texture = <ThreeJsTexture>attachment.region?.texture; texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
attachmentColor = attachment.color; attachmentColor = attachment.color;
vertices = this.vertices; vertices = this.vertices;
@ -267,8 +271,12 @@ export class SkeletonMesh extends THREE.Object3D {
vertexSize vertexSize
); );
triangles = attachment.triangles; triangles = attachment.triangles;
uvs = attachment.uvs;
texture = <ThreeJsTexture>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) { } else if (attachment instanceof ClippingAttachment) {
clipper.clipEnd(slot); clipper.clipEnd(slot);
clipper.clipStart(skeleton, slot, attachment); clipper.clipStart(skeleton, slot, attachment);

View File

@ -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 // we could probably cache the vertices from rendering if interaction with this slot is enabled
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
const regionAttachment = <RegionAttachment>attachment; attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
const mesh = <MeshAttachment>attachment; attachment.computeWorldVertices(this.skeleton as Skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
mesh.computeWorldVertices(this.skeleton as Skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, 2); hullLength = attachment.hullLength;
hullLength = mesh.hullLength;
} }
// here we have only "move" and "drag" events // here we have only "move" and "drag" events

View File

@ -1,86 +0,0 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<script src="../dist/iife/spine-webgl.js"></script>
<style>
* {
margin: 0;
padding: 0;
}
</style>
<body>
<canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
<script>
class App {
constructor() {
this.skeleton = null;
this.animationState = null;
}
loadAssets(canvas) {
// Load the skeleton file.
canvas.assetManager.loadBinary("/assets/dragon-ess.skel");
// Load the atlas and its pages.
canvas.assetManager.loadTextureAtlas("/assets/dragon-pma.atlas");
}
initialize(canvas) {
let assetManager = canvas.assetManager;
// Create the texture atlas.
var atlas = assetManager.require("/assets/dragon-pma.atlas");
// Create a AtlasAttachmentLoader that resolves region, mesh, boundingbox and path attachments
var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
// Create a SkeletonBinary instance for parsing the .skel file.
var skeletonBinary = new spine.SkeletonBinary(atlasLoader);
// Set the scale to apply during parsing, parse the file, and create a new skeleton.
skeletonBinary.scale = 1;
var skeletonData = skeletonBinary.readSkeletonData(assetManager.require("/assets/dragon-ess.skel"));
this.skeleton = new spine.Skeleton(skeletonData);
// Create an AnimationState, and set the "run" animation in looping mode.
var animationStateData = new spine.AnimationStateData(skeletonData);
this.animationState = new spine.AnimationState(animationStateData);
this.animationState.setAnimation(0, "flying", true);
}
update(canvas, delta) {
// Update the animation state using the delta time.
this.animationState.update(delta);
// Apply the animation state to the skeleton.
this.animationState.apply(this.skeleton);
// Let the skeleton update the transforms of its bones.
this.skeleton.updateWorldTransform(spine.Physics.update);
}
render(canvas) {
let renderer = canvas.renderer;
// Resize the viewport to the full canvas.
renderer.resize(spine.ResizeMode.Expand);
// Clear the canvas with a light gray color.
canvas.clear(0.2, 0.2, 0.2, 1);
// Begin rendering.
renderer.begin();
// Draw the skeleton
renderer.drawSkeleton(this.skeleton);
// Complete rendering.
renderer.end();
}
}
new spine.SpineCanvas(document.getElementById("canvas"), {
app: new App()
})
</script>
</body>
</html>

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
</head>
<script src="../dist/iife/spine-webgl.js"></script>
<script src="https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.umd.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/lil-gui@0.20.0/dist/lil-gui.min.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
}
</style>
<body>
<canvas id="canvas" style="position: absolute; width: 100%; height: 100%;"></canvas>
<script>
class App {
constructor() {
this.skeleton1 = null;
this.skeleton2 = null;
this.animationState1 = null;
this.animationState2 = null;
this.manualControl = false;
}
loadAssets(canvas) {
canvas.assetManager.loadBinary("/assets/dragon-ess.skel");
canvas.assetManager.loadTextureAtlas("/assets/dragon-pma.atlas");
}
initialize(canvas) {
let assetManager = canvas.assetManager;
var atlas = assetManager.require("/assets/dragon-pma.atlas");
var atlasLoader = new spine.AtlasAttachmentLoader(atlas);
var skeletonBinary = new spine.SkeletonBinary(atlasLoader);
skeletonBinary.scale = 0.5;
var skeletonData = skeletonBinary.readSkeletonData(assetManager.require("/assets/dragon-ess.skel"));
// First skeleton
this.skeleton1 = new spine.Skeleton(skeletonData);
var animationStateData1 = new spine.AnimationStateData(skeletonData);
this.animationState1 = new spine.AnimationState(animationStateData1);
this.animationState1.setAnimation(0, "flying", true);
// Second skeleton sharing the same skeletonData
this.skeleton2 = new spine.Skeleton(skeletonData);
var animationStateData2 = new spine.AnimationStateData(skeletonData);
this.animationState2 = new spine.AnimationState(animationStateData2);
this.animationState2.setAnimation(0, "flying", true);
// GUI for manual time control
const myObject = { time1: 0, time2: 0 };
let prevValue1 = 0;
let prevValue2 = 0;
const gui = new lil.GUI({});
gui.add(myObject, 'time1').min(0).max(10).step(0.01)
.name('time dragon 1')
.onChange(value => {
this.manualControl = true;
const delta = value - prevValue1;
prevValue1 = value;
this.animationState1.update(delta / 10);
this.animationState1.apply(this.skeleton1);
this.skeleton1.updateWorldTransform(spine.Physics.update);
});
gui.add(myObject, 'time2').min(0).max(10).step(0.01)
.name('time dragon 2')
.onChange(value => {
this.manualControl = true;
const delta = value - prevValue2;
prevValue2 = value;
this.animationState2.update(delta / 10);
this.animationState2.apply(this.skeleton2);
this.skeleton2.updateWorldTransform(spine.Physics.update);
});
}
update(canvas, delta) {
if (this.manualControl) return;
this.animationState1.update(delta);
this.animationState1.apply(this.skeleton1);
this.skeleton1.updateWorldTransform(spine.Physics.update);
this.animationState2.update(delta);
this.animationState2.apply(this.skeleton2);
this.skeleton2.updateWorldTransform(spine.Physics.update);
}
render(canvas) {
let renderer = canvas.renderer;
renderer.resize(spine.ResizeMode.Expand);
canvas.clear(0.2, 0.2, 0.2, 1);
// Position the two skeletons
this.skeleton1.x = 0;
this.skeleton1.y = -100;
this.skeleton2.x = 0;
this.skeleton2.y = 200;
renderer.begin();
renderer.drawSkeleton(this.skeleton1);
renderer.drawSkeleton(this.skeleton2);
renderer.end();
}
}
new spine.SpineCanvas(document.getElementById("canvas"), {
app: new App()
})
</script>
</body>
</html>

View File

@ -91,7 +91,8 @@ export class SkeletonDebugRenderer implements Disposable {
const attachment = slot.applied.attachment; const attachment = slot.applied.attachment;
if (attachment instanceof RegionAttachment) { if (attachment instanceof RegionAttachment) {
const vertices = this.vertices; 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[0], vertices[1], vertices[2], vertices[3]);
shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]); shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]); shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);

View File

@ -27,7 +27,7 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. * 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 { GLTexture } from "./GLTexture.js";
import type { PolygonBatcher } from "./PolygonBatcher.js"; import type { PolygonBatcher } from "./PolygonBatcher.js";
import type { ManagedWebGLRenderingContext } from "./WebGL.js"; import type { ManagedWebGLRenderingContext } from "./WebGL.js";
@ -102,10 +102,14 @@ export class SkeletonRenderer {
renderable.vertices = this.vertices; renderable.vertices = this.vertices;
renderable.numVertices = 4; renderable.numVertices = 4;
renderable.numFloats = vertexSize << 2; 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; triangles = SkeletonRenderer.QUAD_TRIANGLES;
uvs = attachment.uvs; uvs = sequence.getUVs(sequenceIndex);
texture = (attachment.region as TextureRegion).texture as GLTexture; texture = sequence.regions[sequenceIndex]?.texture as GLTexture;
attachmentColor = attachment.color; attachmentColor = attachment.color;
} else if (attachment instanceof MeshAttachment) { } else if (attachment instanceof MeshAttachment) {
renderable.vertices = this.vertices; renderable.vertices = this.vertices;
@ -117,8 +121,12 @@ export class SkeletonRenderer {
} }
attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize); attachment.computeWorldVertices(skeleton, slot, 0, attachment.worldVerticesLength, renderable.vertices, 0, vertexSize);
triangles = attachment.triangles; 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; attachmentColor = attachment.color;
} else if (attachment instanceof ClippingAttachment) { } else if (attachment instanceof ClippingAttachment) {
clipper.clipEnd(slot); clipper.clipEnd(slot);