UNPKG

@esotericsoftware/spine-core

Version:
979 lines (978 loc) 251 kB
/****************************************************************************** * 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, CurveTimeline1, 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, SequenceModeValues } from "./attachments/Sequence.js"; import { BoneData } 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, 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 { 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 binary format. * * See [Spine binary format](http://esotericsoftware.com/spine-binary-format) and * [JSON and binary data](http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data) in the Spine * Runtimes Guide. */ export class SkeletonBinary { /** 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; attachmentLoader; linkedMeshes = []; constructor(attachmentLoader) { this.attachmentLoader = attachmentLoader; } readSkeletonData(binary) { const scale = this.scale; const skeletonData = new SkeletonData(); skeletonData.name = ""; // BOZO const input = new BinaryInput(binary); const lowHash = input.readInt32(); const highHash = input.readInt32(); skeletonData.hash = highHash === 0 && lowHash === 0 ? null : highHash.toString(16) + lowHash.toString(16); skeletonData.version = input.readString(); skeletonData.x = input.readFloat(); skeletonData.y = input.readFloat(); skeletonData.width = input.readFloat(); skeletonData.height = input.readFloat(); skeletonData.referenceScale = input.readFloat() * scale; const nonessential = input.readBoolean(); if (nonessential) { skeletonData.fps = input.readFloat(); skeletonData.imagesPath = input.readString(); skeletonData.audioPath = input.readString(); } let n = 0; // Strings. n = input.readInt(true); for (let i = 0; i < n; i++) { const str = input.readString(); if (!str) throw new Error("String in string table must not be null."); input.strings.push(str); } // Bones. const bones = skeletonData.bones; n = input.readInt(true); for (let i = 0; i < n; i++) { const name = input.readString(); if (!name) throw new Error("Bone name must not be null."); const parent = i === 0 ? null : bones[input.readInt(true)]; const data = new BoneData(i, name, parent); const setup = data.setupPose; setup.rotation = input.readFloat(); setup.x = input.readFloat() * scale; setup.y = input.readFloat() * scale; setup.scaleX = input.readFloat(); setup.scaleY = input.readFloat(); setup.shearX = input.readFloat(); setup.shearY = input.readFloat(); setup.inherit = input.readByte(); data.length = input.readFloat() * scale; data.skinRequired = input.readBoolean(); if (nonessential) { Color.rgba8888ToColor(data.color, input.readInt32()); data.icon = input.readString() ?? undefined; data.iconSize = input.readFloat(); data.iconRotation = input.readFloat(); data.visible = input.readBoolean(); } bones.push(data); } // Slots. n = input.readInt(true); for (let i = 0; i < n; i++) { const slotName = input.readString(); if (!slotName) throw new Error("Slot name must not be null."); const boneData = bones[input.readInt(true)]; const data = new SlotData(i, slotName, boneData); Color.rgba8888ToColor(data.setupPose.color, input.readInt32()); const darkColor = input.readInt32(); if (darkColor !== -1) Color.rgb888ToColor(data.setupPose.darkColor = new Color(), darkColor); data.attachmentName = input.readStringRef(); data.blendMode = input.readInt(true); if (nonessential) data.visible = input.readBoolean(); skeletonData.slots.push(data); } // Constraints. const constraints = skeletonData.constraints; const constraintCount = input.readInt(true); for (let i = 0; i < constraintCount; i++) { const name = input.readString(); if (!name) throw new Error("Constraint data name must not be null."); let nn; switch (input.readByte()) { case CONSTRAINT_IK: { const data = new IkConstraintData(name); nn = input.readInt(true); for (let ii = 0; ii < nn; ii++) data.bones.push(bones[input.readInt(true)]); data.target = bones[input.readInt(true)]; const flags = input.readByte(); data.skinRequired = (flags & 1) !== 0; if ((flags & 2) !== 0) data.scaleYMode = input.readUnsignedByte(); const setup = data.setupPose; setup.bendDirection = (flags & 4) !== 0 ? -1 : 1; setup.compress = (flags & 8) !== 0; setup.stretch = (flags & 16) !== 0; if ((flags & 32) !== 0) setup.mix = (flags & 64) !== 0 ? input.readFloat() : 1; if ((flags & 128) !== 0) setup.softness = input.readFloat() * scale; constraints.push(data); break; } case CONSTRAINT_TRANSFORM: { const data = new TransformConstraintData(name); nn = input.readInt(true); for (let ii = 0; ii < nn; ii++) data.bones.push(bones[input.readInt(true)]); data.source = bones[input.readInt(true)]; let flags = input.readUnsignedByte(); data.skinRequired = (flags & 1) !== 0; data.localSource = (flags & 2) !== 0; data.localTarget = (flags & 4) !== 0; data.additive = (flags & 8) !== 0; data.clamp = (flags & 16) !== 0; nn = flags >> 5; for (let ii = 0, tn; ii < nn; ii++) { let fromScale = 1; let from; switch (input.readByte()) { case 0: from = new FromRotate(); break; case 1: { fromScale = scale; from = new FromX(); break; } case 2: { fromScale = scale; from = new FromY(); break; } case 3: from = new FromScaleX(); break; case 4: from = new FromScaleY(); break; case 5: from = new FromShearY(); break; default: from = null; } if (!from) continue; from.offset = input.readFloat() * fromScale; tn = input.readByte(); for (let t = 0; t < tn; t++) { let toScale = 1; let to; switch (input.readByte()) { case 0: to = new ToRotate(); break; case 1: { toScale = scale; to = new ToX(); break; } case 2: { toScale = scale; to = new ToY(); break; } case 3: to = new ToScaleX(); break; case 4: to = new ToScaleY(); break; case 5: to = new ToShearY(); break; default: to = null; } if (!to) continue; to.offset = input.readFloat() * toScale; to.max = input.readFloat() * toScale; to.scale = input.readFloat() * toScale / fromScale; from.to[t] = to; } data.properties[ii] = from; } flags = input.readByte(); if ((flags & 1) !== 0) data.offsets[TransformConstraintData.ROTATION] = input.readFloat(); if ((flags & 2) !== 0) data.offsets[TransformConstraintData.X] = input.readFloat() * scale; if ((flags & 4) !== 0) data.offsets[TransformConstraintData.Y] = input.readFloat() * scale; if ((flags & 8) !== 0) data.offsets[TransformConstraintData.SCALEX] = input.readFloat(); if ((flags & 16) !== 0) data.offsets[TransformConstraintData.SCALEY] = input.readFloat(); if ((flags & 32) !== 0) data.offsets[TransformConstraintData.SHEARY] = input.readFloat(); flags = input.readByte(); const setup = data.setupPose; if ((flags & 1) !== 0) setup.mixRotate = input.readFloat(); if ((flags & 2) !== 0) setup.mixX = input.readFloat(); if ((flags & 4) !== 0) setup.mixY = input.readFloat(); if ((flags & 8) !== 0) setup.mixScaleX = input.readFloat(); if ((flags & 16) !== 0) setup.mixScaleY = input.readFloat(); if ((flags & 32) !== 0) setup.mixShearY = input.readFloat(); constraints.push(data); break; } case CONSTRAINT_PATH: { const data = new PathConstraintData(name); nn = input.readInt(true); for (let ii = 0; ii < nn; ii++) data.bones.push(bones[input.readInt(true)]); data.slot = skeletonData.slots[input.readInt(true)]; const flags = input.readByte(); data.skinRequired = (flags & 1) !== 0; 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.setupPose; setup.position = input.readFloat(); if (data.positionMode === PositionMode.Fixed) setup.position *= scale; setup.spacing = input.readFloat(); if (data.spacingMode === SpacingMode.Length || data.spacingMode === SpacingMode.Fixed) setup.spacing *= scale; setup.mixRotate = input.readFloat(); setup.mixX = input.readFloat(); setup.mixY = input.readFloat(); constraints.push(data); break; } case CONSTRAINT_PHYSICS: { const data = new PhysicsConstraintData(name); data.bone = bones[input.readInt(true)]; let flags = input.readByte(); data.skinRequired = (flags & 1) !== 0; if ((flags & 2) !== 0) data.x = input.readFloat(); if ((flags & 4) !== 0) data.y = input.readFloat(); if ((flags & 8) !== 0) data.rotate = input.readFloat(); if ((flags & 16) !== 0) { let scaleX = input.readFloat(); if (scaleX < -2) { data.scaleYMode = ScaleYMode.Volume; scaleX = -2 - scaleX; } else if (scaleX < 0) { data.scaleYMode = ScaleYMode.Uniform; scaleX = -1 - scaleX; } data.scaleX = scaleX; } if ((flags & 32) !== 0) data.shearX = input.readFloat(); data.limit = ((flags & 64) !== 0 ? input.readFloat() : 5000) * scale; data.step = 1 / input.readUnsignedByte(); const setup = data.setupPose; setup.inertia = input.readFloat(); setup.strength = input.readFloat(); setup.damping = input.readFloat(); setup.massInverse = (flags & 128) !== 0 ? input.readFloat() : 1; setup.wind = input.readFloat(); setup.gravity = input.readFloat(); flags = input.readByte(); if ((flags & 1) !== 0) data.inertiaGlobal = true; if ((flags & 2) !== 0) data.strengthGlobal = true; if ((flags & 4) !== 0) data.dampingGlobal = true; if ((flags & 8) !== 0) data.massGlobal = true; if ((flags & 16) !== 0) data.windGlobal = true; if ((flags & 32) !== 0) data.gravityGlobal = true; if ((flags & 64) !== 0) data.mixGlobal = true; setup.mix = (flags & 128) !== 0 ? input.readFloat() : 1; constraints.push(data); break; } case CONSTRAINT_SLIDER: { const data = new SliderData(name); const flags = input.readByte(); data.skinRequired = (flags & 1) !== 0; data.loop = (flags & 2) !== 0; data.additive = (flags & 4) !== 0; if ((flags & 8) !== 0) { const value = input.readFloat(); if (nonessential && (flags & 64) !== 0) data.max = value; else data.setupPose.time = value; } if ((flags & 16) !== 0) data.setupPose.mix = (flags & 32) !== 0 ? input.readFloat() : 1; if ((flags & 64) !== 0) { data.local = (flags & 128) !== 0; data.bone = bones[input.readInt(true)]; const offset = input.readFloat(); let propertyScale = 1; switch (input.readByte()) { case 0: data.property = new FromRotate(); break; case 1: { propertyScale = scale; data.property = new FromX(); break; } case 2: { propertyScale = scale; data.property = new FromY(); break; } case 3: data.property = new FromScaleX(); break; case 4: data.property = new FromScaleY(); break; case 5: data.property = new FromShearY(); break; default: continue; } ; data.property.offset = offset * propertyScale; data.offset = input.readFloat(); data.scale = input.readFloat() / propertyScale; } constraints.push(data); break; } } } // Default skin. const defaultSkin = this.readSkin(input, skeletonData, true, nonessential); if (defaultSkin) { skeletonData.defaultSkin = defaultSkin; skeletonData.skins.push(defaultSkin); } // Skins. { let i = skeletonData.skins.length; Utils.setArraySize(skeletonData.skins, n = i + input.readInt(true)); for (; i < n; i++) { const skin = this.readSkin(input, skeletonData, false, nonessential); if (!skin) throw new Error("readSkin() should not have returned null."); skeletonData.skins[i] = skin; } } // Linked meshes. n = this.linkedMeshes.length; for (let i = 0; i < n; i++) { const linkedMesh = this.linkedMeshes[i]; const skin = skeletonData.skins[linkedMesh.skinIndex]; if (!linkedMesh.source) throw new Error("Linked mesh parent must not be null"); 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(); } this.linkedMeshes.length = 0; // Events. n = input.readInt(true); for (let i = 0; i < n; i++) { const eventName = input.readString(); if (!eventName) throw new Error("Event data name must not be null"); const data = new EventData(eventName); const setup = data.setupPose; setup.intValue = input.readInt(false); setup.floatValue = input.readFloat(); setup.stringValue = input.readString(); data._audioPath = input.readString(); if (data.audioPath) { setup.volume = input.readFloat(); setup.balance = input.readFloat(); } skeletonData.events.push(data); } // Animations. const animations = skeletonData.animations; n = input.readInt(true); for (let i = 0; i < n; i++) { const animationName = input.readString(); if (!animationName) throw new Error("Animation name must not be null."); animations.push(this.readAnimation(input, animationName, skeletonData, nonessential)); } for (let i = 0; i < constraintCount; i++) { const constraint = constraints[i]; if (constraint instanceof SliderData) constraint.animation = animations[input.readInt(true)]; } return skeletonData; } readSkin(input, skeletonData, defaultSkin, nonessential) { let skin = null; let slotCount = 0; if (defaultSkin) { slotCount = input.readInt(true); if (slotCount === 0) return null; skin = new Skin("default"); } else { const skinName = input.readString(); if (!skinName) throw new Error("Skin name must not be null."); skin = new Skin(skinName); if (nonessential) Color.rgba8888ToColor(skin.color, input.readInt32()); let n = input.readInt(true); let from = skeletonData.bones, to = skin.bones; for (let i = 0; i < n; i++) to[i] = from[input.readInt(true)]; n = input.readInt(true); from = skeletonData.constraints; to = skin.constraints; for (let i = 0; i < n; i++) to[i] = from[input.readInt(true)]; slotCount = input.readInt(true); } for (let i = 0; i < slotCount; i++) { const slotIndex = input.readInt(true); for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { const placeholder = input.readStringRef(); if (!placeholder) throw new Error("Attachment name must not be null"); const attachment = this.readAttachment(input, skeletonData, skin, slotIndex, placeholder, nonessential); if (attachment) skin.setAttachment(slotIndex, placeholder, attachment); } } return skin; } readAttachment(input, skeletonData, skin, slotIndex, placeholder, nonessential) { const scale = this.scale; const flags = input.readByte(); const name = (flags & 8) !== 0 ? input.readStringRef() : placeholder; if (!name) throw new Error("Attachment name must not be null"); switch ((flags & 0b111)) { // BUG? case AttachmentType.Region: { let path = (flags & 16) !== 0 ? input.readStringRef() : null; const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff; const sequence = this.readSequence(input, (flags & 64) !== 0); const rotation = (flags & 128) !== 0 ? input.readFloat() : 0; const x = input.readFloat(); const y = input.readFloat(); const scaleX = input.readFloat(); const scaleY = input.readFloat(); const width = input.readFloat(); const height = input.readFloat(); if (!path) path = name; const region = this.attachmentLoader.newRegionAttachment(skin, placeholder, name, path, sequence); if (!region) return null; region.path = path; region.x = x * scale; region.y = y * scale; region.scaleX = scaleX; region.scaleY = scaleY; region.rotation = rotation; region.width = width * scale; region.height = height * scale; Color.rgba8888ToColor(region.color, color); region.updateSequence(); return region; } case AttachmentType.BoundingBox: { const vertices = this.readVertices(input, (flags & 16) !== 0); const color = nonessential ? input.readInt32() : 0; const box = this.attachmentLoader.newBoundingBoxAttachment(skin, placeholder, name); if (!box) return null; box.worldVerticesLength = vertices.length; box.vertices = vertices.vertices; box.bones = vertices.bones; if (nonessential) Color.rgba8888ToColor(box.color, color); return box; } case AttachmentType.Mesh: { let path = (flags & 16) !== 0 ? input.readStringRef() : name; const color = (flags & 32) !== 0 ? input.readInt32() : 0xffffffff; 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); const triangles = this.readShortArray(input, (vertices.length - hullLength - 2) * 3); const slotCount = input.readInt(true); let timelineSlots = null; if (slotCount > 0) { timelineSlots = []; for (let i = 0; i < slotCount; i++) timelineSlots[i] = input.readInt(true); } let edges = []; let width = 0, height = 0; if (nonessential) { edges = this.readShortArray(input, input.readInt(true)); width = input.readFloat(); height = input.readFloat(); } if (!path) path = name; const mesh = this.attachmentLoader.newMeshAttachment(skin, placeholder, name, path, sequence); 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.regionUVs = uvs; mesh.triangles = triangles; if (timelineSlots) mesh.timelineSlots = timelineSlots; 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 = this.readSequence(input, (flags & 64) !== 0); const inheritTimelines = (flags & 128) !== 0; const sourceIndex = input.readInt(true); const skinIndex = input.readInt(true); const source = input.readStringRef(); let width = 0, height = 0; if (nonessential) { width = input.readFloat(); height = input.readFloat(); } const mesh = this.attachmentLoader.newMeshAttachment(skin, placeholder, name, path, sequence); if (!mesh) return null; mesh.path = path; Color.rgba8888ToColor(mesh.color, color); if (nonessential) { mesh.width = width * scale; mesh.height = height * scale; } this.linkedMeshes.push(new LinkedMesh(mesh, skinIndex, slotIndex, sourceIndex, source, inheritTimelines)); return mesh; } case AttachmentType.Path: { const closed = (flags & 16) !== 0; const constantSpeed = (flags & 32) !== 0; const vertices = this.readVertices(input, (flags & 64) !== 0); const lengths = this.readFloatArray(input, vertices.length / 6, scale); const color = nonessential ? input.readInt32() : 0; const path = this.attachmentLoader.newPathAttachment(skin, placeholder, name); if (!path) return null; path.closed = closed; path.constantSpeed = constantSpeed; path.worldVerticesLength = vertices.length; path.vertices = vertices.vertices; path.bones = vertices.bones; path.lengths = lengths; if (nonessential) Color.rgba8888ToColor(path.color, color); return path; } case AttachmentType.Point: { const rotation = input.readFloat(); const x = input.readFloat(); const y = input.readFloat(); const color = nonessential ? input.readInt32() : 0; const point = this.attachmentLoader.newPointAttachment(skin, placeholder, name); if (!point) return null; point.x = x * scale; point.y = y * scale; point.rotation = rotation; if (nonessential) Color.rgba8888ToColor(point.color, color); return point; } case AttachmentType.Clipping: { const endSlotIndex = input.readInt(true); const vertices = this.readVertices(input, (flags & 16) !== 0); const color = nonessential ? input.readInt32() : 0; const clip = this.attachmentLoader.newClippingAttachment(skin, placeholder, name); if (!clip) return null; clip.endSlot = skeletonData.slots[endSlotIndex]; clip.convex = (flags & 32) !== 0; clip.inverse = (flags & 64) !== 0; clip.worldVerticesLength = vertices.length; clip.vertices = vertices.vertices; clip.bones = vertices.bones; if (nonessential) Color.rgba8888ToColor(clip.color, color); return clip; } } } readSequence(input, hasPathSuffix) { 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); return sequence; } readVertices(input, weighted) { const scale = this.scale; const vertexCount = input.readInt(true); const length = vertexCount << 1; if (!weighted) return new Vertices(null, this.readFloatArray(input, length, scale), length); const n = input.readInt(true); const bones = []; const weights = []; for (let b = 0, w = 0; b < n;) { const boneCount = input.readInt(true); 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(bones, Utils.toFloatArray(weights), length); } readFloatArray(input, n, scale) { const array = []; if (scale === 1) { for (let i = 0; i < n; i++) array[i] = input.readFloat(); } else { for (let i = 0; i < n; i++) array[i] = input.readFloat() * scale; } return array; } readShortArray(input, n) { const array = []; for (let i = 0; i < n; i++) array[i] = input.readInt(true); return array; } readAnimation(input, name, skeletonData, nonessential) { input.readInt(true); // Number of timelines. const timelines = []; const scale = this.scale; // Slot timelines. for (let i = 0, n = input.readInt(true); i < n; i++) { const slotIndex = input.readInt(true); for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { const timelineType = input.readByte(); const frameCount = input.readInt(true); const frameLast = frameCount - 1; switch (timelineType) { case SLOT_ATTACHMENT: { const timeline = new AttachmentTimeline(frameCount, slotIndex); for (let frame = 0; frame < frameCount; frame++) timeline.setFrame(frame, input.readFloat(), input.readStringRef()); timelines.push(timeline); break; } case SLOT_RGBA: { const bezierCount = input.readInt(true); const timeline = new RGBATimeline(frameCount, bezierCount, slotIndex); let time = input.readFloat(); let r = input.readUnsignedByte() / 255.0; let g = input.readUnsignedByte() / 255.0; let b = input.readUnsignedByte() / 255.0; let a = input.readUnsignedByte() / 255.0; for (let frame = 0, bezier = 0;; frame++) { timeline.setFrame(frame, time, r, g, b, a); if (frame === frameLast) break; const time2 = input.readFloat(); const r2 = input.readUnsignedByte() / 255.0; const g2 = input.readUnsignedByte() / 255.0; const b2 = input.readUnsignedByte() / 255.0; const a2 = input.readUnsignedByte() / 255.0; switch (input.readByte()) { case CURVE_STEPPED: timeline.setStepped(frame); break; case CURVE_BEZIER: setBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1); setBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1); setBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1); setBezier(input, timeline, bezier++, frame, 3, time, time2, a, a2, 1); } time = time2; r = r2; g = g2; b = b2; a = a2; } timelines.push(timeline); break; } case SLOT_RGB: { const bezierCount = input.readInt(true); const timeline = new RGBTimeline(frameCount, bezierCount, slotIndex); let time = input.readFloat(); let r = input.readUnsignedByte() / 255.0; let g = input.readUnsignedByte() / 255.0; let b = input.readUnsignedByte() / 255.0; for (let frame = 0, bezier = 0;; frame++) { timeline.setFrame(frame, time, r, g, b); if (frame === frameLast) break; const time2 = input.readFloat(); const r2 = input.readUnsignedByte() / 255.0; const g2 = input.readUnsignedByte() / 255.0; const b2 = input.readUnsignedByte() / 255.0; switch (input.readByte()) { case CURVE_STEPPED: timeline.setStepped(frame); break; case CURVE_BEZIER: setBezier(input, timeline, bezier++, frame, 0, time, time2, r, r2, 1); setBezier(input, timeline, bezier++, frame, 1, time, time2, g, g2, 1); setBezier(input, timeline, bezier++, frame, 2, time, time2, b, b2, 1); } time = time2; r = r2; g = g2; b = b2; } timelines.push(timeline); break; } case SLOT_RGBA2: { const bezierCount = input.readInt(true); const timeline = new RGBA2Timeline(frameCount, bezierCount, slotIndex); let time = input.readFloat(); let r = input.readUnsignedByte() / 255.0; let g = input.readUnsignedByte() / 255.0; let b = input.readUnsignedByte() / 255.0; let a = input.readUnsignedByte() / 255.0; let r2 = input.readUnsignedByte() / 255.0; let g2 = input.readUnsignedByte() / 255.0; let b2 = input.readUnsignedByte() / 255.0; for (let frame = 0, bezier = 0;; frame++) { timeline.setFrame(frame, time, r, g, b, a, r2, g2, b2); if (frame === frameLast) break; const time2 = input.readFloat(); const nr = input.readUnsignedByte() / 255.0; const ng = input.readUnsignedByte() / 255.0; const nb = input.readUnsignedByte() / 255.0; const na = input.readUnsignedByte() / 255.0; const nr2 = input.readUnsignedByte() / 255.0; const ng2 = input.readUnsignedByte() / 255.0; const nb2 = input.readUnsignedByte() / 255.0; switch (input.readByte()) { case CURVE_STEPPED: timeline.setStepped(frame); break; case CURVE_BEZIER: setBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1); setBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1); setBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1); setBezier(input, timeline, bezier++, frame, 3, time, time2, a, na, 1); setBezier(input, timeline, bezier++, frame, 4, time, time2, r2, nr2, 1); setBezier(input, timeline, bezier++, frame, 5, time, time2, g2, ng2, 1); setBezier(input, timeline, bezier++, frame, 6, time, time2, b2, nb2, 1); } time = time2; r = nr; g = ng; b = nb; a = na; r2 = nr2; g2 = ng2; b2 = nb2; } timelines.push(timeline); break; } case SLOT_RGB2: { const bezierCount = input.readInt(true); const timeline = new RGB2Timeline(frameCount, bezierCount, slotIndex); let time = input.readFloat(); let r = input.readUnsignedByte() / 255.0; let g = input.readUnsignedByte() / 255.0; let b = input.readUnsignedByte() / 255.0; let r2 = input.readUnsignedByte() / 255.0; let g2 = input.readUnsignedByte() / 255.0; let b2 = input.readUnsignedByte() / 255.0; for (let frame = 0, bezier = 0;; frame++) { timeline.setFrame(frame, time, r, g, b, r2, g2, b2); if (frame === frameLast) break; const time2 = input.readFloat(); const nr = input.readUnsignedByte() / 255.0; const ng = input.readUnsignedByte() / 255.0; const nb = input.readUnsignedByte() / 255.0; const nr2 = input.readUnsignedByte() / 255.0; const ng2 = input.readUnsignedByte() / 255.0; const nb2 = input.readUnsignedByte() / 255.0; switch (input.readByte()) { case CURVE_STEPPED: timeline.setStepped(frame); break; case CURVE_BEZIER: setBezier(input, timeline, bezier++, frame, 0, time, time2, r, nr, 1); setBezier(input, timeline, bezier++, frame, 1, time, time2, g, ng, 1); setBezier(input, timeline, bezier++, frame, 2, time, time2, b, nb, 1); setBezier(input, timeline, bezier++, frame, 3, time, time2, r2, nr2, 1); setBezier(input, timeline, bezier++, frame, 4, time, time2, g2, ng2, 1); setBezier(input, timeline, bezier++, frame, 5, time, time2, b2, nb2, 1); } time = time2; r = nr; g = ng; b = nb; r2 = nr2; g2 = ng2; b2 = nb2; } timelines.push(timeline); break; } case SLOT_ALPHA: { const timeline = new AlphaTimeline(frameCount, input.readInt(true), slotIndex); let time = input.readFloat(), a = input.readUnsignedByte() / 255; for (let frame = 0, bezier = 0;; frame++) { timeline.setFrame(frame, time, a); if (frame === frameLast) break; const time2 = input.readFloat(); const a2 = input.readUnsignedByte() / 255; switch (input.readByte()) { case CURVE_STEPPED: timeline.setStepped(frame); break; case CURVE_BEZIER: setBezier(input, timeline, bezier++, frame, 0, time, time2, a, a2, 1); } time = time2; a = a2; } timelines.push(timeline); } } } } // Bone timelines. for (let i = 0, n = input.readInt(true); i < n; i++) { const boneIndex = input.readInt(true); for (let ii = 0, nn = input.readInt(true); ii < nn; ii++) { const type = input.readByte(), frameCount = input.readInt(true); if (type === BONE_INHERIT) { const timeline = new InheritTimeline(frameCount, boneIndex); for (let frame = 0; frame < frameCount; frame++) { timeline.setFrame(frame, input.readFloat(), input.readByte()); } timelines.push(timeline); continue; } const bezierCount = input.readInt(true); switch (type) { case BONE_ROTATE: readTimeline(input, timelines, new RotateTimeline(frameCount, bezierCount, boneIndex), 1); break; case BONE_TRANSLATE: readTimeline(input, timelines, new TranslateTimeline(frameCount, bezierCount, boneIndex), scale); break; case BONE_TRANSLATEX: readTimeline(input, timelines, new TranslateXTimeline(frameCount, bezierCount, boneIndex), scale); break; case BONE_TRANSLATEY: readTimeline(input, timelines, new TranslateYTimeline(frameCount, bezierCount, boneIndex), scale); break; case BONE_SCALE: readTimeline(input, timelines, new ScaleTimeline(frameCount, bezierCount, boneIndex), 1); break; case BONE_SCALEX: readTimeline(input, timelines, new ScaleXTimeline(frameCount, bezierCount, boneIndex), 1); break; case BONE_SCALEY: readTimeline(input, timelines, new ScaleYTimeline(frameCount, bezierCount, boneIndex), 1); break; case BONE_SHEAR: