@esotericsoftware/spine-core
Version:
The official Spine Runtimes for the web.
868 lines • 253 kB
JavaScript
/******************************************************************************
* Spine Runtimes License Agreement
* Last updated April 5, 2025. Replaces all prior versions.
*
* Copyright (c) 2013-2025, Esoteric Software LLC
*
* Integration of the Spine Runtimes into software or otherwise creating
* derivative works of the Spine Runtimes is permitted under the terms and
* conditions of Section 2 of the Spine Editor License Agreement:
* http://esotericsoftware.com/spine-editor-license
*
* Otherwise, it is permitted to integrate the Spine Runtimes into software
* or otherwise create derivative works of the Spine Runtimes (collectively,
* "Products"), provided that each user of the Products must obtain their own
* Spine Editor license and redistribution of the Products in any form must
* include this license and copyright notice.
*
* THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY
* DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES,
* BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
* THE SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*****************************************************************************/
import { AlphaTimeline, Animation, AttachmentTimeline, DeformTimeline, DrawOrderFolderTimeline, 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, TransformConstraintTimeline, TranslateTimeline, TranslateXTimeline, TranslateYTimeline } from "./Animation.js";
import { Sequence, SequenceMode } from "./attachments/Sequence.js";
import { BoneData, Inherit } from "./BoneData.js";
import { ScaleYMode } from "./ConstraintData.js";
import { Event } from "./Event.js";
import { EventData } from "./EventData.js";
import { IkConstraintData } from "./IkConstraintData.js";
import { PathConstraintData, PositionMode, RotateMode, SpacingMode } from "./PathConstraintData.js";
import { PhysicsConstraintData } from "./PhysicsConstraintData.js";
import { SkeletonData } from "./SkeletonData.js";
import { Skin } from "./Skin.js";
import { SliderData } from "./SliderData.js";
import { BlendMode, SlotData } from "./SlotData.js";
import { FromRotate, FromScaleX, FromScaleY, FromShearY, FromX, FromY, ToRotate, ToScaleX, ToScaleY, ToShearY, ToX, ToY, TransformConstraintData } from "./TransformConstraintData.js";
import { Color, Utils } from "./Utils.js";
/** Loads skeleton data in the Spine JSON format.
*
* See [Spine JSON format](http://esotericsoftware.com/spine-json-format) and
* [JSON and binary data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the Spine
* Runtimes Guide. */
export class SkeletonJson {
attachmentLoader;
/** Scales bone positions, image sizes, and translations as they are loaded. This allows different size images to be used at
* runtime than were used in Spine.
*
* See [Scaling](http://esotericsoftware.com/spine-loading-skeleton-data#Scaling) in the Spine Runtimes Guide. */
scale = 1;
linkedMeshes = [];
constructor(attachmentLoader) {
this.attachmentLoader = attachmentLoader;
}
// biome-ignore lint/suspicious/noExplicitAny: it is any until we define a schema
readSkeletonData(json) {
const scale = this.scale;
const skeletonData = new SkeletonData();
const root = typeof (json) === "string" ? JSON.parse(json) : json;
// Skeleton
const skeletonMap = root.skeleton;
if (skeletonMap) {
skeletonData.hash = skeletonMap.hash;
skeletonData.version = skeletonMap.spine;
skeletonData.x = skeletonMap.x;
skeletonData.y = skeletonMap.y;
skeletonData.width = skeletonMap.width;
skeletonData.height = skeletonMap.height;
skeletonData.referenceScale = getValue(skeletonMap, "referenceScale", 100) * scale;
skeletonData.fps = skeletonMap.fps;
skeletonData.imagesPath = skeletonMap.images ?? null;
skeletonData.audioPath = skeletonMap.audio ?? null;
}
// Bones
if (root.bones) {
for (let i = 0; i < root.bones.length; i++) {
const boneMap = root.bones[i];
let parent = null;
const parentName = getValue(boneMap, "parent", null);
if (parentName)
parent = skeletonData.findBone(parentName);
const data = new BoneData(skeletonData.bones.length, boneMap.name, parent);
data.length = getValue(boneMap, "length", 0) * scale;
const setup = data.setupPose;
setup.x = getValue(boneMap, "x", 0) * scale;
setup.y = getValue(boneMap, "y", 0) * scale;
setup.rotation = getValue(boneMap, "rotation", 0);
setup.scaleX = getValue(boneMap, "scaleX", 1);
setup.scaleY = getValue(boneMap, "scaleY", 1);
setup.shearX = getValue(boneMap, "shearX", 0);
setup.shearY = getValue(boneMap, "shearY", 0);
setup.inherit = Utils.enumValue(Inherit, getValue(boneMap, "inherit", "Normal"));
data.skinRequired = getValue(boneMap, "skin", false);
const color = getValue(boneMap, "color", null);
if (color)
data.color.setFromString(color);
data.icon = getValue(boneMap, "icon", undefined);
data.iconSize = getValue(boneMap, "iconSize", 1);
data.iconRotation = getValue(boneMap, "iconRotation", 0);
skeletonData.bones.push(data);
}
}
// Slots.
if (root.slots) {
for (let i = 0; i < root.slots.length; i++) {
const slotMap = root.slots[i];
const slotName = slotMap.name;
const boneData = skeletonData.findBone(slotMap.bone);
if (!boneData)
throw new Error(`Couldn't find bone ${slotMap.bone} for slot ${slotName}`);
const data = new SlotData(skeletonData.slots.length, slotName, boneData);
const color = getValue(slotMap, "color", null);
if (color)
data.setupPose.color.setFromString(color);
const dark = getValue(slotMap, "dark", null);
if (dark)
data.setupPose.darkColor = Color.fromString(dark);
data.attachmentName = getValue(slotMap, "attachment", null);
data.blendMode = Utils.enumValue(BlendMode, getValue(slotMap, "blend", "normal"));
data.visible = getValue(slotMap, "visible", true);
skeletonData.slots.push(data);
}
}
// Constraints.
if (root.constraints) {
for (const constraintMap of root.constraints) {
const name = constraintMap.name;
const skinRequired = getValue(constraintMap, "skin", false);
switch (getValue(constraintMap, "type", false)) {
case "ik": {
const data = new IkConstraintData(name);
data.skinRequired = skinRequired;
for (let ii = 0; ii < constraintMap.bones.length; ii++) {
const bone = skeletonData.findBone(constraintMap.bones[ii]);
if (!bone)
throw new Error(`Couldn't find bone ${constraintMap.bones[ii]} for IK constraint ${name}.`);
data.bones.push(bone);
}
const targetName = constraintMap.target;
const target = skeletonData.findBone(targetName);
if (!target)
throw new Error(`Couldn't find target bone ${targetName} for IK constraint ${name}.`);
data.target = target;
const scaleY = getValue(constraintMap, "scaleY", null);
if (scaleY != null)
data.scaleYMode = Utils.enumValue(ScaleYMode, scaleY);
const setup = data.setupPose;
setup.mix = getValue(constraintMap, "mix", 1);
setup.softness = getValue(constraintMap, "softness", 0) * scale;
setup.bendDirection = getValue(constraintMap, "bendPositive", true) ? 1 : -1;
setup.compress = getValue(constraintMap, "compress", false);
setup.stretch = getValue(constraintMap, "stretch", false);
skeletonData.constraints.push(data);
break;
}
case "transform": {
const data = new TransformConstraintData(name);
data.skinRequired = skinRequired;
for (let ii = 0; ii < constraintMap.bones.length; ii++) {
const boneName = constraintMap.bones[ii];
const bone = skeletonData.findBone(boneName);
if (!bone)
throw new Error(`Couldn't find bone ${boneName} for transform constraint ${constraintMap.name}.`);
data.bones.push(bone);
}
const sourceName = constraintMap.source;
const source = skeletonData.findBone(sourceName);
if (!source)
throw new Error(`Couldn't find source bone ${sourceName} for transform constraint ${constraintMap.name}.`);
data.source = source;
data.localSource = getValue(constraintMap, "localSource", false);
data.localTarget = getValue(constraintMap, "localTarget", false);
data.additive = getValue(constraintMap, "additive", false);
data.clamp = getValue(constraintMap, "clamp", false);
let rotate = false, x = false, y = false, scaleX = false, scaleY = false, shearY = false;
const fromEntries = Object.entries(getValue(constraintMap, "properties", {}));
for (const [name, fromEntry] of fromEntries) {
const from = this.fromProperty(name);
const fromScale = this.propertyScale(name, scale);
from.offset = getValue(fromEntry, "offset", 0) * fromScale;
const toEntries = Object.entries(getValue(fromEntry, "to", {}));
for (const [name, toEntry] of toEntries) {
let toScale = 1;
let to;
switch (name) {
case "rotate": {
rotate = true;
to = new ToRotate();
break;
}
case "x": {
x = true;
to = new ToX();
toScale = scale;
break;
}
case "y": {
y = true;
to = new ToY();
toScale = scale;
break;
}
case "scaleX": {
scaleX = true;
to = new ToScaleX();
break;
}
case "scaleY": {
scaleY = true;
to = new ToScaleY();
break;
}
case "shearY": {
shearY = true;
to = new ToShearY();
break;
}
default: throw new Error(`Invalid transform constraint to property: ${name}`);
}
to.offset = getValue(toEntry, "offset", 0) * toScale;
to.max = getValue(toEntry, "max", 1) * toScale;
to.scale = getValue(toEntry, "scale", 1) * toScale / fromScale;
from.to.push(to);
}
if (from.to.length > 0)
data.properties.push(from);
}
data.offsets[TransformConstraintData.ROTATION] = getValue(constraintMap, "rotation", 0);
data.offsets[TransformConstraintData.X] = getValue(constraintMap, "x", 0) * scale;
data.offsets[TransformConstraintData.Y] = getValue(constraintMap, "y", 0) * scale;
data.offsets[TransformConstraintData.SCALEX] = getValue(constraintMap, "scaleX", 0);
data.offsets[TransformConstraintData.SCALEY] = getValue(constraintMap, "scaleY", 0);
data.offsets[TransformConstraintData.SHEARY] = getValue(constraintMap, "shearY", 0);
const setup = data.setupPose;
if (rotate)
setup.mixRotate = getValue(constraintMap, "mixRotate", 1);
if (x)
setup.mixX = getValue(constraintMap, "mixX", 1);
if (y)
setup.mixY = getValue(constraintMap, "mixY", setup.mixX);
if (scaleX)
setup.mixScaleX = getValue(constraintMap, "mixScaleX", 1);
if (scaleY)
setup.mixScaleY = getValue(constraintMap, "mixScaleY", setup.mixScaleX);
if (shearY)
setup.mixShearY = getValue(constraintMap, "mixShearY", 1);
skeletonData.constraints.push(data);
break;
}
case "path": {
const data = new PathConstraintData(name);
data.skinRequired = skinRequired;
for (let ii = 0; ii < constraintMap.bones.length; ii++) {
const boneName = constraintMap.bones[ii];
const bone = skeletonData.findBone(boneName);
if (!bone)
throw new Error(`Couldn't find bone ${boneName} for path constraint ${constraintMap.name}.`);
data.bones.push(bone);
}
const slotName = constraintMap.slot;
const slot = skeletonData.findSlot(slotName);
if (!slot)
throw new Error(`Couldn't find slot ${slotName} for path constraint ${constraintMap.name}.`);
data.slot = slot;
data.positionMode = Utils.enumValue(PositionMode, getValue(constraintMap, "positionMode", "Percent"));
data.spacingMode = Utils.enumValue(SpacingMode, getValue(constraintMap, "spacingMode", "Length"));
data.rotateMode = Utils.enumValue(RotateMode, getValue(constraintMap, "rotateMode", "Tangent"));
data.offsetRotation = getValue(constraintMap, "rotation", 0);
const setup = data.setupPose;
setup.position = getValue(constraintMap, "position", 0);
if (data.positionMode === PositionMode.Fixed)
setup.position *= scale;
setup.spacing = getValue(constraintMap, "spacing", 0);
if (data.spacingMode === SpacingMode.Length || data.spacingMode === SpacingMode.Fixed)
setup.spacing *= scale;
setup.mixRotate = getValue(constraintMap, "mixRotate", 1);
setup.mixX = getValue(constraintMap, "mixX", 1);
setup.mixY = getValue(constraintMap, "mixY", setup.mixX);
skeletonData.constraints.push(data);
break;
}
case "physics": {
const data = new PhysicsConstraintData(name);
data.skinRequired = skinRequired;
const boneName = constraintMap.bone;
const bone = skeletonData.findBone(boneName);
if (bone == null)
throw new Error(`Physics bone not found: ${boneName}`);
data.bone = bone;
data.x = getValue(constraintMap, "x", 0);
data.y = getValue(constraintMap, "y", 0);
data.rotate = getValue(constraintMap, "rotate", 0);
data.scaleX = getValue(constraintMap, "scaleX", 0);
const scaleY = getValue(constraintMap, "scaleY", null);
if (scaleY != null)
data.scaleYMode = Utils.enumValue(ScaleYMode, scaleY);
data.shearX = getValue(constraintMap, "shearX", 0);
data.limit = getValue(constraintMap, "limit", 5000) * scale;
data.step = 1 / getValue(constraintMap, "fps", 60);
const setup = data.setupPose;
setup.inertia = getValue(constraintMap, "inertia", 0.5);
setup.strength = getValue(constraintMap, "strength", 100);
setup.damping = getValue(constraintMap, "damping", 0.85);
setup.massInverse = 1 / getValue(constraintMap, "mass", 1);
setup.wind = getValue(constraintMap, "wind", 0);
setup.gravity = getValue(constraintMap, "gravity", 0);
setup.mix = getValue(constraintMap, "mix", 1);
data.inertiaGlobal = getValue(constraintMap, "inertiaGlobal", false);
data.strengthGlobal = getValue(constraintMap, "strengthGlobal", false);
data.dampingGlobal = getValue(constraintMap, "dampingGlobal", false);
data.massGlobal = getValue(constraintMap, "massGlobal", false);
data.windGlobal = getValue(constraintMap, "windGlobal", false);
data.gravityGlobal = getValue(constraintMap, "gravityGlobal", false);
data.mixGlobal = getValue(constraintMap, "mixGlobal", false);
skeletonData.constraints.push(data);
break;
}
case "slider": {
const data = new SliderData(name);
data.skinRequired = skinRequired;
data.additive = getValue(constraintMap, "additive", false);
data.loop = getValue(constraintMap, "loop", false);
data.setupPose.mix = getValue(constraintMap, "mix", 1);
const boneName = constraintMap.bone;
if (boneName) {
data.bone = skeletonData.findBone(boneName);
if (!data.bone)
throw new Error(`Slider bone not found: ${boneName}`);
const property = constraintMap.property;
data.property = this.fromProperty(property);
const propertyScale = this.propertyScale(property, scale);
data.property.offset = getValue(constraintMap, "from", 0) * propertyScale;
data.offset = getValue(constraintMap, "to", 0);
data.scale = getValue(constraintMap, "scale", 1) / propertyScale;
data.max = getValue(constraintMap, "max", 0);
data.local = getValue(constraintMap, "local", false);
}
else
data.setupPose.time = getValue(constraintMap, "time", 0);
skeletonData.constraints.push(data);
break;
}
}
}
}
// Skins.
if (root.skins) {
for (let i = 0; i < root.skins.length; i++) {
const skinMap = root.skins[i];
const skin = new Skin(skinMap.name);
if (skinMap.bones) {
for (let ii = 0; ii < skinMap.bones.length; ii++) {
const boneName = skinMap.bones[ii];
const bone = skeletonData.findBone(boneName);
if (!bone)
throw new Error(`Couldn't find bone ${boneName} for skin ${skinMap.name}.`);
skin.bones.push(bone);
}
}
if (skinMap.ik) {
for (let ii = 0; ii < skinMap.ik.length; ii++) {
const constraintName = skinMap.ik[ii];
const constraint = skeletonData.findConstraint(constraintName, IkConstraintData);
if (!constraint)
throw new Error(`Couldn't find IK constraint ${constraintName} for skin ${skinMap.name}.`);
skin.constraints.push(constraint);
}
}
if (skinMap.transform) {
for (let ii = 0; ii < skinMap.transform.length; ii++) {
const constraintName = skinMap.transform[ii];
const constraint = skeletonData.findConstraint(constraintName, TransformConstraintData);
if (!constraint)
throw new Error(`Couldn't find transform constraint ${constraintName} for skin ${skinMap.name}.`);
skin.constraints.push(constraint);
}
}
if (skinMap.path) {
for (let ii = 0; ii < skinMap.path.length; ii++) {
const constraintName = skinMap.path[ii];
const constraint = skeletonData.findConstraint(constraintName, PathConstraintData);
if (!constraint)
throw new Error(`Couldn't find path constraint ${constraintName} for skin ${skinMap.name}.`);
skin.constraints.push(constraint);
}
}
if (skinMap.physics) {
for (let ii = 0; ii < skinMap.physics.length; ii++) {
const constraintName = skinMap.physics[ii];
const constraint = skeletonData.findConstraint(constraintName, PhysicsConstraintData);
if (!constraint)
throw new Error(`Couldn't find physics constraint ${constraintName} for skin ${skinMap.name}.`);
skin.constraints.push(constraint);
}
}
if (skinMap.slider) {
for (let ii = 0; ii < skinMap.slider.length; ii++) {
const constraintName = skinMap.slider[ii];
const constraint = skeletonData.findConstraint(constraintName, SliderData);
if (!constraint)
throw new Error(`Couldn't find slider constraint ${constraintName} for skin ${skinMap.name}.`);
skin.constraints.push(constraint);
}
}
for (const slotName in skinMap.attachments) {
const slot = skeletonData.findSlot(slotName);
if (!slot)
throw new Error(`Couldn't find skin slot ${slotName} for skin ${skinMap.name}.`);
const slotMap = skinMap.attachments[slotName];
for (const entryName in slotMap) {
const attachment = this.readAttachment(slotMap[entryName], skin, slot.index, entryName, skeletonData);
if (attachment)
skin.setAttachment(slot.index, entryName, attachment);
}
}
skeletonData.skins.push(skin);
if (skin.name === "default")
skeletonData.defaultSkin = skin;
}
}
// Linked meshes.
for (let i = 0, n = this.linkedMeshes.length; i < n; i++) {
const linkedMesh = this.linkedMeshes[i];
const skin = !linkedMesh.skin ? skeletonData.defaultSkin : skeletonData.findSkin(linkedMesh.skin);
if (!skin)
throw new Error(`Skin not found: ${linkedMesh.skin}`);
const source = skin.getAttachment(linkedMesh.sourceIndex, linkedMesh.source);
if (!source)
throw new Error(`Source mesh not found: ${linkedMesh.source}`);
linkedMesh.mesh.timelineAttachment = linkedMesh.inheritTimelines ? source : linkedMesh.mesh;
linkedMesh.mesh.setSourceMesh(source);
linkedMesh.mesh.updateSequence();
// biome-ignore lint/suspicious/noConfusingLabels: reference runtime
outer: if (linkedMesh.inheritTimelines && linkedMesh.slotIndex !== linkedMesh.sourceIndex) {
const slots = source.timelineSlots;
for (const existing of slots)
if (existing === linkedMesh.slotIndex)
break outer;
const newSlots = [...slots];
newSlots[slots.length] = linkedMesh.slotIndex;
source.timelineSlots = newSlots;
}
}
this.linkedMeshes.length = 0;
// Events.
if (root.events) {
for (const eventName in root.events) {
const eventMap = root.events[eventName];
const data = new EventData(eventName);
const setup = data.setupPose;
setup.intValue = getValue(eventMap, "int", 0);
setup.floatValue = getValue(eventMap, "float", 0);
setup.stringValue = getValue(eventMap, "string", "");
data._audioPath = getValue(eventMap, "audio", null);
if (data.audioPath) {
setup.volume = getValue(eventMap, "volume", setup.volume);
setup.balance = getValue(eventMap, "balance", setup.balance);
}
skeletonData.events.push(data);
}
}
// Animations.
if (root.animations) {
for (const animationName in root.animations) {
const animationMap = root.animations[animationName];
this.readAnimation(animationMap, animationName, skeletonData);
}
}
// Slider animations.
if (root.constraints) {
for (const animationName in root.constraints) {
const animationMap = root.constraints[animationName];
if (animationMap.type === "slider") {
const data = skeletonData.findConstraint(animationMap.name, SliderData);
const animationName = animationMap.animation;
const animation = skeletonData.findAnimation(animationName);
if (!animation)
throw new Error(`Slider animation not found: ${animationName}`);
// biome-ignore lint/style/noNonNullAssertion: reference runtime
data.animation = animation;
}
}
}
return skeletonData;
}
fromProperty(type) {
let from;
switch (type) {
case "rotate":
from = new FromRotate();
break;
case "x":
from = new FromX();
break;
case "y":
from = new FromY();
break;
case "scaleX":
from = new FromScaleX();
break;
case "scaleY":
from = new FromScaleY();
break;
case "shearY":
from = new FromShearY();
break;
default: throw new Error(`Invalid transform constraint from property: ${type}`);
}
return from;
}
propertyScale(type, scale) {
switch (type) {
case "x":
case "y": return scale;
default: return 1;
}
}
// biome-ignore lint/suspicious/noExplicitAny: it is any until we define a schema
readAttachment(map, skin, slotIndex, placeholder, skeletonData) {
const scale = this.scale;
const name = getValue(map, "name", placeholder);
switch (getValue(map, "type", "region")) {
case "region": {
const path = getValue(map, "path", name);
const sequence = this.readSequence(getValue(map, "sequence", null));
const region = this.attachmentLoader.newRegionAttachment(skin, placeholder, name, path, sequence);
if (!region)
return null;
region.path = path;
region.x = getValue(map, "x", 0) * scale;
region.y = getValue(map, "y", 0) * scale;
region.scaleX = getValue(map, "scaleX", 1);
region.scaleY = getValue(map, "scaleY", 1);
region.rotation = getValue(map, "rotation", 0);
region.width = map.width * scale;
region.height = map.height * scale;
const color = getValue(map, "color", null);
if (color)
region.color.setFromString(color);
region.updateSequence();
return region;
}
case "boundingbox": {
const box = this.attachmentLoader.newBoundingBoxAttachment(skin, placeholder, name);
if (!box)
return null;
this.readVertices(map, box, map.vertexCount << 1);
const color = getValue(map, "color", null);
if (color)
box.color.setFromString(color);
return box;
}
case "mesh":
case "linkedmesh": {
const path = getValue(map, "path", name);
const sequence = this.readSequence(getValue(map, "sequence", null));
const mesh = this.attachmentLoader.newMeshAttachment(skin, placeholder, name, path, sequence);
if (!mesh)
return null;
mesh.path = path;
const color = getValue(map, "color", null);
if (color)
mesh.color.setFromString(color);
mesh.width = getValue(map, "width", 0) * scale;
mesh.height = getValue(map, "height", 0) * scale;
const source = getValue(map, "source", null);
if (source) {
let sourceIndex = slotIndex;
const slot = getValue(map, "slot", null);
if (slot) {
const sourceSlot = skeletonData.findSlot(slot);
if (!sourceSlot)
throw new Error(`Source mesh slot not found: ${slot}`);
sourceIndex = sourceSlot.index;
}
this.linkedMeshes.push(new LinkedMesh(mesh, getValue(map, "skin", null), slotIndex, sourceIndex, source, getValue(map, "timelines", true)));
return mesh;
}
const uvs = map.uvs;
this.readVertices(map, mesh, uvs.length);
mesh.triangles = map.triangles;
mesh.regionUVs = uvs;
mesh.edges = getValue(map, "edges", null);
mesh.hullLength = getValue(map, "hull", 0) * 2;
mesh.updateSequence();
return mesh;
}
case "path": {
const path = this.attachmentLoader.newPathAttachment(skin, placeholder, name);
if (!path)
return null;
path.closed = getValue(map, "closed", false);
path.constantSpeed = getValue(map, "constantSpeed", true);
const vertexCount = map.vertexCount;
this.readVertices(map, path, vertexCount << 1);
const lengths = Utils.newArray(vertexCount / 3, 0);
for (let i = 0; i < map.lengths.length; i++)
lengths[i] = map.lengths[i] * scale;
path.lengths = lengths;
const color = getValue(map, "color", null);
if (color)
path.color.setFromString(color);
return path;
}
case "point": {
const point = this.attachmentLoader.newPointAttachment(skin, placeholder, name);
if (!point)
return null;
point.x = getValue(map, "x", 0) * scale;
point.y = getValue(map, "y", 0) * scale;
point.rotation = getValue(map, "rotation", 0);
const color = getValue(map, "color", null);
if (color)
point.color.setFromString(color);
return point;
}
case "clipping": {
const clip = this.attachmentLoader.newClippingAttachment(skin, placeholder, name);
if (!clip)
return null;
const end = getValue(map, "end", null);
if (end)
clip.endSlot = skeletonData.findSlot(end);
clip.convex = getValue(map, "convex", false);
clip.inverse = getValue(map, "inverse", false);
const vertexCount = map.vertexCount;
this.readVertices(map, clip, vertexCount << 1);
const color = getValue(map, "color", null);
if (color)
clip.color.setFromString(color);
return clip;
}
}
return null;
}
readSequence(map) {
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);
return sequence;
}
// biome-ignore lint/suspicious/noExplicitAny: it is any until we define a schema
readVertices(map, attachment, verticesLength) {
const scale = this.scale;
attachment.worldVerticesLength = verticesLength;
const vertices = map.vertices;
if (verticesLength === vertices.length) {
const scaledVertices = Utils.toFloatArray(vertices);
if (scale !== 1) {
for (let i = 0, n = vertices.length; i < n; i++)
scaledVertices[i] *= scale;
}
attachment.vertices = scaledVertices;
return;
}
const weights = [];
const bones = [];
for (let i = 0, n = vertices.length; i < n;) {
const boneCount = vertices[i++];
bones.push(boneCount);
for (let nn = i + boneCount * 4; i < nn; i += 4) {
bones.push(vertices[i]);
weights.push(vertices[i + 1] * scale);
weights.push(vertices[i + 2] * scale);
weights.push(vertices[i + 3]);
}
}
attachment.bones = bones;
attachment.vertices = Utils.toFloatArray(weights);
}
// biome-ignore lint/suspicious/noExplicitAny: it is any untile we define a schema
readAnimation(map, name, skeletonData) {
const scale = this.scale;
const timelines = [];
// Slot timelines.
if (map.slots) {
for (const slotName in map.slots) {
const slotMap = map.slots[slotName];
const slot = skeletonData.findSlot(slotName);
if (!slot)
throw new Error(`Slot not found: ${slotName}`);
const slotIndex = slot.index;
for (const timelineName in slotMap) {
const timelineMap = slotMap[timelineName];
if (!timelineMap)
continue;
const frames = timelineMap.length;
switch (timelineName) {
case "attachment": {
const timeline = new AttachmentTimeline(frames, slotIndex);
for (let frame = 0; frame < frames; frame++) {
const keyMap = timelineMap[frame];
timeline.setFrame(frame, getValue(keyMap, "time", 0), getValue(keyMap, "name", null));
}
timelines.push(timeline);
break;
}
case "rgba": {
const timeline = new RGBATimeline(frames, frames << 2, slotIndex);
let keyMap = timelineMap[0];
let time = getValue(keyMap, "time", 0);
let color = Color.fromString(keyMap.color);
for (let frame = 0, bezier = 0;; frame++) {
timeline.setFrame(frame, time, color.r, color.g, color.b, color.a);
const nextMap = timelineMap[frame + 1];
if (!nextMap) {
timeline.shrink(bezier);
break;
}
const time2 = getValue(nextMap, "time", 0);
const newColor = Color.fromString(nextMap.color);
const curve = keyMap.curve;
if (curve) {
bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1);
bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1);
bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1);
bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color.a, newColor.a, 1);
}
time = time2;
color = newColor;
keyMap = nextMap;
}
timelines.push(timeline);
break;
}
case "rgb": {
const timeline = new RGBTimeline(frames, frames * 3, slotIndex);
let keyMap = timelineMap[0];
let time = getValue(keyMap, "time", 0);
let color = Color.fromString(keyMap.color);
for (let frame = 0, bezier = 0;; frame++) {
timeline.setFrame(frame, time, color.r, color.g, color.b);
const nextMap = timelineMap[frame + 1];
if (!nextMap) {
timeline.shrink(bezier);
break;
}
const time2 = getValue(nextMap, "time", 0);
const newColor = Color.fromString(nextMap.color);
const curve = keyMap.curve;
if (curve) {
bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1);
bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1);
bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1);
}
time = time2;
color = newColor;
keyMap = nextMap;
}
timelines.push(timeline);
break;
}
case "alpha": {
readTimeline1(timelines, timelineMap, new AlphaTimeline(frames, frames, slotIndex), 0, 1);
break;
}
case "rgba2": {
const timeline = new RGBA2Timeline(frames, frames * 7, slotIndex);
let keyMap = timelineMap[0];
let time = getValue(keyMap, "time", 0);
let color = Color.fromString(keyMap.light);
let color2 = Color.fromString(keyMap.dark);
for (let frame = 0, bezier = 0;; frame++) {
timeline.setFrame(frame, time, color.r, color.g, color.b, color.a, color2.r, color2.g, color2.b);
const nextMap = timelineMap[frame + 1];
if (!nextMap) {
timeline.shrink(bezier);
break;
}
const time2 = getValue(nextMap, "time", 0);
const newColor = Color.fromString(nextMap.light);
const newColor2 = Color.fromString(nextMap.dark);
const curve = keyMap.curve;
if (curve) {
bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1);
bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1);
bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1);
bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color.a, newColor.a, 1);
bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, color2.r, newColor2.r, 1);
bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, color2.g, newColor2.g, 1);
bezier = readCurve(curve, timeline, bezier, frame, 6, time, time2, color2.b, newColor2.b, 1);
}
time = time2;
color = newColor;
color2 = newColor2;
keyMap = nextMap;
}
timelines.push(timeline);
break;
}
case "rgb2": {
const timeline = new RGB2Timeline(frames, frames * 6, slotIndex);
let keyMap = timelineMap[0];
let time = getValue(keyMap, "time", 0);
let color = Color.fromString(keyMap.light);
let color2 = Color.fromString(keyMap.dark);
for (let frame = 0, bezier = 0;; frame++) {
timeline.setFrame(frame, time, color.r, color.g, color.b, color2.r, color2.g, color2.b);
const nextMap = timelineMap[frame + 1];
if (!nextMap) {
timeline.shrink(bezier);
break;
}
const time2 = getValue(nextMap, "time", 0);
const newColor = Color.fromString(nextMap.light);
const newColor2 = Color.fromString(nextMap.dark);
const curve = keyMap.curve;
if (curve) {
bezier = readCurve(curve, timeline, bezier, frame, 0, time, time2, color.r, newColor.r, 1);
bezier = readCurve(curve, timeline, bezier, frame, 1, time, time2, color.g, newColor.g, 1);
bezier = readCurve(curve, timeline, bezier, frame, 2, time, time2, color.b, newColor.b, 1);
bezier = readCurve(curve, timeline, bezier, frame, 3, time, time2, color2.r, newColor2.r, 1);
bezier = readCurve(curve, timeline, bezier, frame, 4, time, time2, color2.g, newColor2.g, 1);
bezier = readCurve(curve, timeline, bezier, frame, 5, time, time2, color2.b, newColor2.b, 1);
}
time = time2;
color = newColor;
color2 = newColor2;
keyMap = nextMap;
}
timelines.push(timeline);
break;
}
default:
throw new Error(`Invalid timeline type for a slot: ${timelineMap.name} (${slotMap.name})`);
}
}
}
}
// Bone timelines.
if (map.bones) {
for (const boneName in map.bones) {
const boneMap = map.bones[boneName];
const bone = skeletonData.findBone(boneName);
if (!bone)
throw new Error(`Bone not found: ${boneName}`);
const boneIndex = bone.index;
for (const timelineName in boneMap) {
const timelineMap = boneMap[timelineName];
const frames = timelineMap.length;
if (frames === 0)
continue;
switch (timelineName) {
case "rotate":
readTimeline1(timelines, timelineMap, new RotateTimeline(frames, frames, boneIndex), 0, 1);
break;
case "translate":
readTimeline2(timelines, timelineMap, new TranslateTimeline(frames, frames << 1, boneIndex), "x", "y", 0, scale);
break;
case "translatex":
readTimeline1(timelines, timelineMap, new TranslateXTimeline(frames, frames, boneIndex), 0, scale);
break;
case "translatey":
readTimeline1(timelines, timelineMap, new TranslateYTimeline(frames, frames, boneIndex), 0, scale);
break;
case "scale":
readTimeline2(timelines, timelineMap, new ScaleTimeline(frames, frames << 1, boneIndex), "x", "y", 1, 1);
break;
case "scalex":
readTimeline1(timelines, timelineMap, new ScaleXTimeline(frame