[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/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/dragon.html">Dragon</a> - (<a href="/spine-pixi-v7/example/dragon.html">v7</a>)</li>
</ul>
<li>Phaser</li>
<ul>
@ -142,6 +143,7 @@
<ul>
<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/dragon.html">Dragon</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/physics3.html">Physics III</a></li>

View File

@ -27,7 +27,7 @@
* 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";
const worldVertices = Utils.newFloatArray(8);
@ -68,10 +68,14 @@ export class SkeletonRenderer {
const pose = slot.applied;
const attachment = pose.attachment;
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 regionColor = attachment.color;
@ -83,7 +87,8 @@ export class SkeletonRenderer {
ctx.save();
const boneApplied = bone.applied;
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);
const atlasScale = attachment.width / region.originalWidth;
@ -91,7 +96,7 @@ export class SkeletonRenderer {
let w = region.width, h = region.height;
ctx.translate(w / 2, h / 2);
if (attachment.region?.degrees === 90) {
if (region.degrees === 90) {
const t = w;
w = h;
h = t;
@ -124,15 +129,25 @@ export class SkeletonRenderer {
let texture: HTMLImageElement;
if (attachment instanceof RegionAttachment) {
const regionAttachment = <RegionAttachment>attachment;
vertices = this.computeRegionVertices(slot, regionAttachment, false);
const sequence = attachment.sequence;
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;
texture = (<CanvasTexture>regionAttachment.region?.texture).getImage() as HTMLImageElement;
texture = (sequence.regions[sequenceIndex]?.texture as CanvasTexture).getImage();
} else if (attachment instanceof MeshAttachment) {
const mesh = <MeshAttachment>attachment;
vertices = this.computeMeshVertices(slot, mesh, false);
triangles = mesh.triangles;
texture = (<CanvasTexture>mesh.region?.texture).getImage() as HTMLImageElement;
const sequence = attachment.sequence;
const sequenceIndex = sequence.resolveIndex(pose);
const uvs = sequence.getUVs(sequenceIndex);
vertices = this.computeMeshVertices(slot, attachment, uvs, false);
triangles = attachment.triangles;
texture = (sequence.regions[sequenceIndex]?.texture as CanvasTexture).getImage();
} else
continue;
@ -226,7 +241,7 @@ export class SkeletonRenderer {
ctx.restore();
}
private computeRegionVertices (slot: Slot, region: RegionAttachment, pma: boolean) {
private computeRegionVertices (slot: Slot, region: RegionAttachment, offsets: NumberArrayLike, uvs: NumberArrayLike, pma: boolean) {
const skeletonColor = slot.skeleton.color;
const slotColor = slot.applied.color;
const regionColor = region.color;
@ -238,10 +253,9 @@ export class SkeletonRenderer {
skeletonColor.b * slotColor.b * regionColor.b * multiplier,
alpha);
region.computeWorldVertices(slot, this.vertices, 0, SkeletonRenderer.VERTEX_SIZE);
region.computeWorldVertices(slot, offsets, this.vertices, 0, SkeletonRenderer.VERTEX_SIZE);
const vertices = this.vertices;
const uvs = region.uvs;
vertices[RegionAttachment.C1R] = color.r;
vertices[RegionAttachment.C1G] = color.g;
@ -274,7 +288,7 @@ export class SkeletonRenderer {
return vertices;
}
private computeMeshVertices (slot: Slot, mesh: MeshAttachment, pma: boolean) {
private computeMeshVertices (slot: Slot, mesh: MeshAttachment, uvs: NumberArrayLike, pma: boolean) {
const skeleton = slot.skeleton;
const skeletonColor = skeleton.color;
const slotColor = slot.applied.color;
@ -292,7 +306,7 @@ export class SkeletonRenderer {
if (vertices.length < mesh.worldVerticesLength) this.vertices = vertices = Utils.newFloatArray(mesh.worldVerticesLength);
mesh.computeWorldVertices(skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, SkeletonRenderer.VERTEX_SIZE);
const uvs = mesh.uvs;
for (let i = 0, u = 0, v = 2; i < vertexCount; i++) {
vertices[v++] = color.r;
vertices[v++] = color.g;

View File

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

View File

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

View File

@ -51,40 +51,27 @@ export class AtlasAttachmentLoader implements AttachmentLoader {
this.allowMissingRegions = allowMissingRegions;
}
loadSequence (name: string, basePath: string, sequence: Sequence) {
protected findRegions (name: string, basePath: string, sequence: Sequence) {
const regions = sequence.regions;
for (let i = 0, n = regions.length; i < n; i++) {
const path = sequence.getPath(basePath, i);
regions[i] = this.atlas.findRegion(path);
if (regions[i] == null && !this.allowMissingRegions)
throw new Error(`Region not found in atlas: ${path} (sequence: ${name})`);
}
for (let i = 0, n = regions.length; i < n; i++)
regions[i] = this.findRegion(name, sequence.getPath(basePath, i));
}
protected findRegion (name: string, path: string) {
const region = this.atlas.findRegion(path);
if (!region && !this.allowMissingRegions)
throw new Error(`Region not found in atlas: ${path} (attachment: ${name})`);
return region;
}
newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence): RegionAttachment {
const attachment = new RegionAttachment(name, path);
if (sequence != null) {
this.loadSequence(name, path, sequence);
} else {
const region = this.atlas.findRegion(path);
if (region == null && !this.allowMissingRegions)
throw new Error(`Region not found in atlas: ${path} (region attachment: ${name})`);
attachment.region = region;
}
return attachment;
this.findRegions(name, path, sequence);
return new RegionAttachment(name, sequence);
}
newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment {
const attachment = new MeshAttachment(name, path);
if (sequence != null) {
this.loadSequence(name, path, sequence);
} else {
const region = this.atlas.findRegion(path);
if (region == null && !this.allowMissingRegions)
throw new Error(`Region not found in atlas: ${path} (mesh attachment: ${name})`);
attachment.region = region;
}
return attachment;
this.findRegions(name, path, sequence);
return new MeshAttachment(name, sequence);
}
newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment {

View File

@ -210,7 +210,7 @@ export class IkConstraint extends Constraint<IkConstraint, IkConstraintData, IkC
cwx = a * child.x + b * child.y + parent.worldX;
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;
a = pp.a;
b = pp.b;

View File

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

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

View File

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

View File

@ -35,9 +35,14 @@ import { type NumberArrayLike, Utils } from "../Utils.js";
export abstract class Attachment {
name: string;
/** Timelines for the timeline attachment are also applied to this attachment.
* @return May be null if no attachment-specific timelines should be applied. */
timelineAttachment?: Attachment;
constructor (name: string) {
if (!name) throw new Error("name cannot be null.");
this.name = name;
this.timelineAttachment = this;
}
abstract copy (): Attachment;
@ -65,10 +70,6 @@ export abstract class VertexAttachment extends Attachment {
* {@link computeWorldVertices} using the `count` parameter. */
worldVerticesLength = 0;
/** Timelines for the timeline attachment are also applied to this attachment.
* May be null if no attachment-specific timelines should be applied. */
timelineAttachment: Attachment = this;
constructor (name: string) {
super(name);
}

View File

@ -42,10 +42,10 @@ import type { Sequence } from "./Sequence.js";
* Runtimes Guide. */
export interface AttachmentLoader {
/** @return May be null to not load an attachment. */
newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence | null): RegionAttachment;
newRegionAttachment (skin: Skin, name: string, path: string, sequence: Sequence): RegionAttachment;
/** @return May be null to not load an attachment. */
newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence | null): MeshAttachment;
newMeshAttachment (skin: Skin, name: string, path: string, sequence: Sequence): MeshAttachment;
/** @return May be null to not load an attachment. */
newBoundingBoxAttachment (skin: Skin, name: string): BoundingBoxAttachment;

View File

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

View File

@ -27,74 +27,123 @@
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import type { Skeleton } from "src/Skeleton.js";
import type { Slot } from "../Slot.js";
import type { TextureRegion } from "../Texture.js";
import { TextureAtlasRegion } from "../TextureAtlas.js";
import { Color, type NumberArrayLike, Utils } from "../Utils.js";
import { type Attachment, VertexAttachment } from "./Attachment.js";
import type { HasTextureRegion } from "./HasTextureRegion.js";
import type { HasSequence } from "./HasSequence.js";
import type { Sequence } from "./Sequence.js";
/** An attachment that displays a textured mesh. A mesh has hull vertices and internal vertices within the hull. Holes are not
* supported. Each vertex has UVs (texture coordinates) and triangles are used to map an image on to the mesh.
*
* See [Mesh attachments](http://esotericsoftware.com/spine-meshes) in the Spine User Guide. */
export class MeshAttachment extends VertexAttachment implements HasTextureRegion {
region: TextureRegion | null = null;
/** The name of the texture region for this attachment. */
path: string;
export class MeshAttachment extends VertexAttachment implements HasSequence {
readonly sequence: Sequence;
/** The UV pair for each vertex, normalized within the texture region. */
regionUVs: NumberArrayLike = [];
/** The UV pair for each vertex, normalized within the entire texture.
*
* See {@link #updateUVs}. */
uvs: NumberArrayLike = [];
/** Triplets of vertex indices which describe the mesh's triangulation. */
triangles: Array<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. */
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. */
width: number = 0;
/** The height of the mesh's image. Available only when nonessential data was exported. */
height: number = 0;
/** The number of entries at the beginning of {@link #vertices} that make up the mesh hull. */
hullLength: number = 0;
/** Vertex index pairs describing edges for controling triangulation. Mesh triangles will never cross edges. Only available if
* nonessential data was exported. Triangulation is not performed at runtime. */
edges: Array<number> = [];
private parentMesh: MeshAttachment | null = null;
sequence: Sequence | null = null;
tempColor = new Color(0, 0, 0, 0);
constructor (name: string, path: string) {
constructor (name: string, sequence: Sequence) {
super(name);
this.path = path;
this.sequence = sequence;
}
/** Calculates {@link #uvs} using the {@link #regionUVs} and region. Must be called if the region, the region's properties, or
* the {@link #regionUVs} are changed. */
updateRegion () {
if (!this.region) throw new Error("Region not set.");
const regionUVs = this.regionUVs;
if (!this.uvs || this.uvs.length !== regionUVs.length) this.uvs = Utils.newFloatArray(regionUVs.length);
const uvs = this.uvs;
const n = this.uvs.length;
let u = this.region.u, v = this.region.v, width = 0, height = 0;
if (this.region instanceof TextureAtlasRegion) {
const region = this.region, page = region.page;
copy (): Attachment {
if (this.parentMesh) return this.newLinkedMesh();
const copy = new MeshAttachment(this.name, this.sequence.copy());
copy.path = this.path;
copy.color.setFromColor(this.color);
this.copyTo(copy);
copy.regionUVs = [];
Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length);
copy.triangles = [];
Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length);
copy.hullLength = this.hullLength;
// Nonessential.
if (this.edges) {
copy.edges = [];
Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length);
}
copy.width = this.width;
copy.height = this.height;
return copy;
}
updateSequence () {
this.sequence.update(this);
}
/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
* {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
* parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
getParentMesh () {
return this.parentMesh;
}
/** @param parentMesh May be null. */
setParentMesh (parentMesh: MeshAttachment) {
this.parentMesh = parentMesh;
if (parentMesh) {
this.bones = parentMesh.bones;
this.vertices = parentMesh.vertices;
this.worldVerticesLength = parentMesh.worldVerticesLength;
this.regionUVs = parentMesh.regionUVs;
this.triangles = parentMesh.triangles;
this.hullLength = parentMesh.hullLength;
this.worldVerticesLength = parentMesh.worldVerticesLength
}
}
/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/
newLinkedMesh (): MeshAttachment {
const copy = new MeshAttachment(this.name, this.sequence.copy());
copy.timelineAttachment = this.timelineAttachment;
copy.path = this.path;
copy.color.setFromColor(this.color);
copy.setParentMesh(this.parentMesh ? this.parentMesh : this);
copy.updateSequence();
return copy;
}
/** Computes {@link Sequence#getUVs(int) UVs} for a mesh attachment.
* @param uvs Output array for the computed UVs, same length as regionUVs. */
static computeUVs (region: TextureRegion | null, regionUVs: NumberArrayLike, uvs: NumberArrayLike): void {
if (!region) throw new Error("Region not set.");
const n = uvs.length;
let u = region.u, v = region.v, width = 0, height = 0;
if (region instanceof TextureAtlasRegion) {
const page = region.page;
const textureWidth = page.width, textureHeight = page.height;
switch (region.degrees) {
case 90:
@ -133,12 +182,12 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
width = region.originalWidth / textureWidth;
height = region.originalHeight / textureHeight;
}
} else if (!this.region) {
} else if (!region) {
u = v = 0;
width = height = 1;
} else {
width = this.region.u2 - u;
height = this.region.v2 - v;
width = region.u2 - u;
height = region.v2 - v;
}
for (let i = 0; i < n; i += 2) {
@ -146,70 +195,4 @@ export class MeshAttachment extends VertexAttachment implements HasTextureRegion
uvs[i + 1] = v + regionUVs[i + 1] * height;
}
}
/** The parent mesh if this is a linked mesh, else null. A linked mesh shares the {@link #bones}, {@link #vertices},
* {@link #regionUVs}, {@link #triangles}, {@link #hullLength}, {@link #edges}, {@link #width}, and {@link #height} with the
* parent mesh, but may have a different {@link #name} or {@link #path} (and therefore a different texture). */
getParentMesh () {
return this.parentMesh;
}
/** @param parentMesh May be null. */
setParentMesh (parentMesh: MeshAttachment) {
this.parentMesh = parentMesh;
if (parentMesh) {
this.bones = parentMesh.bones;
this.vertices = parentMesh.vertices;
this.worldVerticesLength = parentMesh.worldVerticesLength;
this.regionUVs = parentMesh.regionUVs;
this.triangles = parentMesh.triangles;
this.hullLength = parentMesh.hullLength;
this.worldVerticesLength = parentMesh.worldVerticesLength
}
}
copy (): Attachment {
if (this.parentMesh) return this.newLinkedMesh();
const copy = new MeshAttachment(this.name, this.path);
copy.region = this.region;
copy.color.setFromColor(this.color);
this.copyTo(copy);
copy.regionUVs = [];
Utils.arrayCopy(this.regionUVs, 0, copy.regionUVs, 0, this.regionUVs.length);
copy.uvs = this.uvs instanceof Float32Array ? Utils.newFloatArray(this.uvs.length) : [];
Utils.arrayCopy(this.uvs, 0, copy.uvs, 0, this.uvs.length);
copy.triangles = [];
Utils.arrayCopy(this.triangles, 0, copy.triangles, 0, this.triangles.length);
copy.hullLength = this.hullLength;
copy.sequence = this.sequence != null ? this.sequence.copy() : null;
// Nonessential.
if (this.edges) {
copy.edges = [];
Utils.arrayCopy(this.edges, 0, copy.edges, 0, this.edges.length);
}
copy.width = this.width;
copy.height = this.height;
return copy;
}
computeWorldVertices (skeleton: Skeleton, slot: Slot, start: number, count: number, worldVertices: NumberArrayLike, offset: number, stride: number) {
if (this.sequence != null) this.sequence.apply(slot.applied, this);
super.computeWorldVertices(skeleton, slot, start, count, worldVertices, offset, stride);
}
/** Returns a new mesh with the {@link #parentMesh} set to this mesh's parent mesh, if any, else to this mesh. **/
newLinkedMesh (): MeshAttachment {
const copy = new MeshAttachment(this.name, this.path);
copy.region = this.region;
copy.color.setFromColor(this.color);
copy.timelineAttachment = this.timelineAttachment;
copy.setParentMesh(this.parentMesh ? this.parentMesh : this);
if (copy.region != null) copy.updateRegion();
return copy;
}
}

View File

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

View File

@ -29,45 +29,103 @@
import type { SlotPose } from "src/SlotPose.js";
import type { TextureRegion } from "../Texture.js";
import { Utils } from "../Utils.js";
import type { HasTextureRegion } from "./HasTextureRegion.js";
import { type NumberArrayLike, Utils } from "../Utils.js";
import type { HasSequence } from "./HasSequence.js";
import { MeshAttachment } from "./MeshAttachment.js";
import { RegionAttachment } from "./RegionAttachment.js";
/** Holds texture regions, UVs, and vertex offsets for rendering a region or mesh attachment. {@link #getRegions() Regions} must
* be populated and {@link #update(HasSequence)} called before use. */
export class Sequence {
private static _nextID = 0;
id = Sequence.nextID();
regions: Array<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;
digits = 0;
/** The index of the region to show for the setup pose. */
setupIndex = 0;
constructor (count: number) {
constructor (count: number, pathSuffix: boolean) {
this.regions = new Array<TextureRegion>(count);
this.pathSuffix = pathSuffix;
}
copy (): Sequence {
const copy = new Sequence(this.regions.length);
Utils.arrayCopy(this.regions, 0, copy.regions, 0, this.regions.length);
const regionCount = this.regions.length;
const copy = new Sequence(regionCount, this.pathSuffix);
Utils.arrayCopy(this.regions, 0, copy.regions, 0, regionCount);
copy.start = this.start;
copy.digits = this.digits;
copy.setupIndex = this.setupIndex;
if (this.uvs != null) {
const length = this.uvs[0].length;
copy.uvs = [];
for (let i = 0; i < regionCount; i++) {
copy.uvs[i] = Utils.newFloatArray(length);
Utils.arrayCopy(this.uvs[i], 0, copy.uvs[i], 0, length);
}
}
if (this.offsets != null) {
copy.offsets = [];
for (let i = 0; i < regionCount; i++) {
copy.offsets[i] = [];
Utils.arrayCopy(this.offsets[i], 0, copy.offsets[i], 0, 8);
}
}
return copy;
}
apply (slot: SlotPose, attachment: HasTextureRegion) {
let index = slot.sequenceIndex;
if (index === -1) index = this.setupIndex;
if (index >= this.regions.length) index = this.regions.length - 1;
const region = this.regions[index];
if (attachment.region !== region) {
attachment.region = region;
attachment.updateRegion();
/** Computes UVs and offsets for the specified attachment. Must be called if the regions or attachment properties are
* changed. */
public update (attachment: HasSequence) {
const regionCount = this.regions.length;
if (attachment instanceof RegionAttachment) {
this.uvs = [];
this.offsets = [];
for (let i = 0; i < regionCount; i++) {
this.uvs[i] = Utils.newFloatArray(8);
this.offsets[i] = [];
RegionAttachment.computeUVs(this.regions[i], attachment.x, attachment.y, attachment.scaleX, attachment.scaleY, attachment.rotation,
attachment.width, attachment.height, this.offsets[i], this.uvs[i]);
}
} else if (attachment instanceof MeshAttachment) {
const regionUVs = attachment.regionUVs;
this.uvs = [];
this.offsets = undefined;
for (let i = 0; i < regionCount; i++) {
this.uvs[i] = Utils.newFloatArray(regionUVs.length);
MeshAttachment.computeUVs(this.regions[i], regionUVs, this.uvs[i]);
}
}
}
resolveIndex (pose: SlotPose): number {
let index = pose.sequenceIndex;
if (index === -1) index = this.setupIndex;
if (index >= this.regions.length) index = this.regions.length - 1;
return index;
}
getUVs (index: number): Float32Array {
// biome-ignore lint/style/noNonNullAssertion: uvs are always defined after updateSequence
return this.uvs![index] as Float32Array;
}
public hasPathSuffix (): boolean {
return this.pathSuffix;
}
getPath (basePath: string, index: number): string {
if (!this.pathSuffix) return basePath;
let result = basePath;
const frame = (this.start + index).toString();
for (let i = this.digits - frame.length; i > 0; i--)

View File

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

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

View File

@ -345,11 +345,9 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
continue;
}
const regionAttachment = attachment;
const vertices = new Float32Array(8);
regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
debugDisplayObjects.regionAttachmentsShape.drawPolygon(Array.from(vertices.slice(0, 8)));
}
}

View File

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

View File

@ -366,12 +366,9 @@ export class SpineDebugRenderer implements ISpineDebugRenderer {
continue;
}
const regionAttachment = attachment;
const vertices = new Float32Array(8);
regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
debugDisplayObjects.regionAttachmentsShape.poly(Array.from(vertices.slice(0, 8)));
}

View File

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

View File

@ -246,10 +246,14 @@ export class SkeletonMesh extends THREE.Object3D {
attachmentColor = attachment.color;
vertices = this.vertices;
numFloats = vertexSize * 4;
attachment.computeWorldVertices(slot, vertices, 0, vertexSize);
const sequence = attachment.sequence;
const sequenceIndex = sequence.resolveIndex(pose);
attachment.computeWorldVertices(slot, attachment.getOffsets(pose), vertices, 0, vertexSize);
triangles = SkeletonMesh.QUAD_TRIANGLES;
uvs = attachment.uvs;
texture = <ThreeJsTexture>attachment.region?.texture;
uvs = sequence.getUVs(sequenceIndex);
texture = sequence.regions[sequenceIndex]?.texture;
} else if (attachment instanceof MeshAttachment) {
attachmentColor = attachment.color;
vertices = this.vertices;
@ -267,8 +271,12 @@ export class SkeletonMesh extends THREE.Object3D {
vertexSize
);
triangles = attachment.triangles;
uvs = attachment.uvs;
texture = <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) {
clipper.clipEnd(slot);
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
if (attachment instanceof RegionAttachment) {
const regionAttachment = <RegionAttachment>attachment;
regionAttachment.computeWorldVertices(slot, vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
} else if (attachment instanceof MeshAttachment) {
const mesh = <MeshAttachment>attachment;
mesh.computeWorldVertices(this.skeleton as Skeleton, slot, 0, mesh.worldVerticesLength, vertices, 0, 2);
hullLength = mesh.hullLength;
attachment.computeWorldVertices(this.skeleton as Skeleton, slot, 0, attachment.worldVerticesLength, vertices, 0, 2);
hullLength = attachment.hullLength;
}
// here we have only "move" and "drag" events

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;
if (attachment instanceof RegionAttachment) {
const vertices = this.vertices;
attachment.computeWorldVertices(slot, vertices, 0, 2);
attachment.computeWorldVertices(slot, attachment.getOffsets(slot.applied), vertices, 0, 2);
shapes.line(vertices[0], vertices[1], vertices[2], vertices[3]);
shapes.line(vertices[2], vertices[3], vertices[4], vertices[5]);
shapes.line(vertices[4], vertices[5], vertices[6], vertices[7]);

View File

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