/****************************************************************************** * Spine Runtimes Software License v2.5 * * Copyright (c) 2013-2016, Esoteric Software * All rights reserved. * * You are granted a perpetual, non-exclusive, non-sublicensable, and * non-transferable license to use, install, execute, and perform the Spine * Runtimes software and derivative works solely for personal or internal * use. Without the written permission of Esoteric Software (see Section 2 of * the Spine Software License Agreement), you may not (a) modify, translate, * adapt, or develop new applications using the Spine Runtimes or otherwise * create derivative works or improvements of the Spine Runtimes or (b) remove, * delete, alter, or obscure any trademarks or any copyright, trademark, patent, * or other intellectual property or proprietary rights notices on or in the * Software, including any copy thereof. Redistributions in binary or source * form must include this license and terms. * * THIS SOFTWARE IS PROVIDED BY ESOTERIC SOFTWARE "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 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 THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ package com.esotericsoftware.spine; import com.badlogic.gdx.files.FileHandle; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.g2d.TextureAtlas; import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.FloatArray; import com.badlogic.gdx.utils.IntArray; import com.badlogic.gdx.utils.JsonReader; import com.badlogic.gdx.utils.JsonValue; import com.badlogic.gdx.utils.SerializationException; import com.esotericsoftware.spine.Animation.AttachmentTimeline; import com.esotericsoftware.spine.Animation.ColorTimeline; import com.esotericsoftware.spine.Animation.CurveTimeline; import com.esotericsoftware.spine.Animation.DeformTimeline; import com.esotericsoftware.spine.Animation.DrawOrderTimeline; import com.esotericsoftware.spine.Animation.EventTimeline; import com.esotericsoftware.spine.Animation.IkConstraintTimeline; import com.esotericsoftware.spine.Animation.PathConstraintMixTimeline; import com.esotericsoftware.spine.Animation.PathConstraintPositionTimeline; import com.esotericsoftware.spine.Animation.PathConstraintSpacingTimeline; import com.esotericsoftware.spine.Animation.RotateTimeline; import com.esotericsoftware.spine.Animation.ScaleTimeline; import com.esotericsoftware.spine.Animation.ShearTimeline; import com.esotericsoftware.spine.Animation.Timeline; import com.esotericsoftware.spine.Animation.TransformConstraintTimeline; import com.esotericsoftware.spine.Animation.TranslateTimeline; import com.esotericsoftware.spine.BoneData.TransformMode; import com.esotericsoftware.spine.PathConstraintData.PositionMode; import com.esotericsoftware.spine.PathConstraintData.RotateMode; import com.esotericsoftware.spine.PathConstraintData.SpacingMode; import com.esotericsoftware.spine.attachments.AtlasAttachmentLoader; import com.esotericsoftware.spine.attachments.Attachment; import com.esotericsoftware.spine.attachments.AttachmentLoader; import com.esotericsoftware.spine.attachments.AttachmentType; import com.esotericsoftware.spine.attachments.BoundingBoxAttachment; import com.esotericsoftware.spine.attachments.MeshAttachment; import com.esotericsoftware.spine.attachments.PathAttachment; import com.esotericsoftware.spine.attachments.RegionAttachment; import com.esotericsoftware.spine.attachments.VertexAttachment; /** Loads skeleton data in the Spine JSON format. * <p> * See <a href="http://esotericsoftware.com/spine-json-format">Spine JSON format</a> and * <a href="http://esotericsoftware.com/spine-loading-skeleton-data#JSON-and-binary-data">JSON and binary data</a> in the Spine * Runtimes Guide. */ public class SkeletonJson { private final AttachmentLoader attachmentLoader; private float scale = 1; private Array<LinkedMesh> linkedMeshes = new Array(); public SkeletonJson (TextureAtlas atlas) { attachmentLoader = new AtlasAttachmentLoader(atlas); } public SkeletonJson (AttachmentLoader attachmentLoader) { if (attachmentLoader == null) throw new IllegalArgumentException("attachmentLoader cannot be null."); this.attachmentLoader = 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. * <p> * See <a href="http://esotericsoftware.com/spine-loading-skeleton-data#Scaling">Scaling</a> in the Spine Runtimes Guide. */ public float getScale () { return scale; } public void setScale (float scale) { this.scale = scale; } public SkeletonData readSkeletonData (FileHandle file) { if (file == null) throw new IllegalArgumentException("file cannot be null."); float scale = this.scale; SkeletonData skeletonData = new SkeletonData(); skeletonData.name = file.nameWithoutExtension(); JsonValue root = new JsonReader().parse(file); // Skeleton. JsonValue skeletonMap = root.get("skeleton"); if (skeletonMap != null) { skeletonData.hash = skeletonMap.getString("hash", null); skeletonData.version = skeletonMap.getString("spine", null); skeletonData.width = skeletonMap.getFloat("width", 0); skeletonData.height = skeletonMap.getFloat("height", 0); skeletonData.fps = skeletonMap.getFloat("fps", 30); skeletonData.imagesPath = skeletonMap.getString("images", null); } // Bones. for (JsonValue boneMap = root.getChild("bones"); boneMap != null; boneMap = boneMap.next) { BoneData parent = null; String parentName = boneMap.getString("parent", null); if (parentName != null) { parent = skeletonData.findBone(parentName); if (parent == null) throw new SerializationException("Parent bone not found: " + parentName); } BoneData data = new BoneData(skeletonData.bones.size, boneMap.getString("name"), parent); data.length = boneMap.getFloat("length", 0) * scale; data.x = boneMap.getFloat("x", 0) * scale; data.y = boneMap.getFloat("y", 0) * scale; data.rotation = boneMap.getFloat("rotation", 0); data.scaleX = boneMap.getFloat("scaleX", 1); data.scaleY = boneMap.getFloat("scaleY", 1); data.shearX = boneMap.getFloat("shearX", 0); data.shearY = boneMap.getFloat("shearY", 0); data.transformMode = TransformMode.valueOf(boneMap.getString("transform", TransformMode.normal.name())); String color = boneMap.getString("color", null); if (color != null) data.getColor().set(Color.valueOf(color)); skeletonData.bones.add(data); } // Slots. for (JsonValue slotMap = root.getChild("slots"); slotMap != null; slotMap = slotMap.next) { String slotName = slotMap.getString("name"); String boneName = slotMap.getString("bone"); BoneData boneData = skeletonData.findBone(boneName); if (boneData == null) throw new SerializationException("Slot bone not found: " + boneName); SlotData data = new SlotData(skeletonData.slots.size, slotName, boneData); String color = slotMap.getString("color", null); if (color != null) data.getColor().set(Color.valueOf(color)); data.attachmentName = slotMap.getString("attachment", null); data.blendMode = BlendMode.valueOf(slotMap.getString("blend", BlendMode.normal.name())); skeletonData.slots.add(data); } // IK constraints. for (JsonValue constraintMap = root.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) { IkConstraintData data = new IkConstraintData(constraintMap.getString("name")); data.order = constraintMap.getInt("order", 0); for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) { String boneName = boneMap.asString(); BoneData bone = skeletonData.findBone(boneName); if (bone == null) throw new SerializationException("IK bone not found: " + boneName); data.bones.add(bone); } String targetName = constraintMap.getString("target"); data.target = skeletonData.findBone(targetName); if (data.target == null) throw new SerializationException("IK target bone not found: " + targetName); data.bendDirection = constraintMap.getBoolean("bendPositive", true) ? 1 : -1; data.mix = constraintMap.getFloat("mix", 1); skeletonData.ikConstraints.add(data); } // Transform constraints. for (JsonValue constraintMap = root.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) { TransformConstraintData data = new TransformConstraintData(constraintMap.getString("name")); data.order = constraintMap.getInt("order", 0); for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) { String boneName = boneMap.asString(); BoneData bone = skeletonData.findBone(boneName); if (bone == null) throw new SerializationException("Transform constraint bone not found: " + boneName); data.bones.add(bone); } String targetName = constraintMap.getString("target"); data.target = skeletonData.findBone(targetName); if (data.target == null) throw new SerializationException("Transform constraint target bone not found: " + targetName); data.offsetRotation = constraintMap.getFloat("rotation", 0); data.offsetX = constraintMap.getFloat("x", 0) * scale; data.offsetY = constraintMap.getFloat("y", 0) * scale; data.offsetScaleX = constraintMap.getFloat("scaleX", 0); data.offsetScaleY = constraintMap.getFloat("scaleY", 0); data.offsetShearY = constraintMap.getFloat("shearY", 0); data.rotateMix = constraintMap.getFloat("rotateMix", 1); data.translateMix = constraintMap.getFloat("translateMix", 1); data.scaleMix = constraintMap.getFloat("scaleMix", 1); data.shearMix = constraintMap.getFloat("shearMix", 1); skeletonData.transformConstraints.add(data); } // Path constraints. for (JsonValue constraintMap = root.getChild("path"); constraintMap != null; constraintMap = constraintMap.next) { PathConstraintData data = new PathConstraintData(constraintMap.getString("name")); data.order = constraintMap.getInt("order", 0); for (JsonValue boneMap = constraintMap.getChild("bones"); boneMap != null; boneMap = boneMap.next) { String boneName = boneMap.asString(); BoneData bone = skeletonData.findBone(boneName); if (bone == null) throw new SerializationException("Path bone not found: " + boneName); data.bones.add(bone); } String targetName = constraintMap.getString("target"); data.target = skeletonData.findSlot(targetName); if (data.target == null) throw new SerializationException("Path target slot not found: " + targetName); data.positionMode = PositionMode.valueOf(constraintMap.getString("positionMode", "percent")); data.spacingMode = SpacingMode.valueOf(constraintMap.getString("spacingMode", "length")); data.rotateMode = RotateMode.valueOf(constraintMap.getString("rotateMode", "tangent")); data.offsetRotation = constraintMap.getFloat("rotation", 0); data.position = constraintMap.getFloat("position", 0); if (data.positionMode == PositionMode.fixed) data.position *= scale; data.spacing = constraintMap.getFloat("spacing", 0); if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) data.spacing *= scale; data.rotateMix = constraintMap.getFloat("rotateMix", 1); data.translateMix = constraintMap.getFloat("translateMix", 1); skeletonData.pathConstraints.add(data); } // Skins. for (JsonValue skinMap = root.getChild("skins"); skinMap != null; skinMap = skinMap.next) { Skin skin = new Skin(skinMap.name); for (JsonValue slotEntry = skinMap.child; slotEntry != null; slotEntry = slotEntry.next) { SlotData slot = skeletonData.findSlot(slotEntry.name); if (slot == null) throw new SerializationException("Slot not found: " + slotEntry.name); for (JsonValue entry = slotEntry.child; entry != null; entry = entry.next) { try { Attachment attachment = readAttachment(entry, skin, slot.index, entry.name); if (attachment != null) skin.addAttachment(slot.index, entry.name, attachment); } catch (Exception ex) { throw new SerializationException("Error reading attachment: " + entry.name + ", skin: " + skin, ex); } } } skeletonData.skins.add(skin); if (skin.name.equals("default")) skeletonData.defaultSkin = skin; } // Linked meshes. for (int i = 0, n = linkedMeshes.size; i < n; i++) { LinkedMesh linkedMesh = linkedMeshes.get(i); Skin skin = linkedMesh.skin == null ? skeletonData.getDefaultSkin() : skeletonData.findSkin(linkedMesh.skin); if (skin == null) throw new SerializationException("Skin not found: " + linkedMesh.skin); Attachment parent = skin.getAttachment(linkedMesh.slotIndex, linkedMesh.parent); if (parent == null) throw new SerializationException("Parent mesh not found: " + linkedMesh.parent); linkedMesh.mesh.setParentMesh((MeshAttachment)parent); linkedMesh.mesh.updateUVs(); } linkedMeshes.clear(); // Events. for (JsonValue eventMap = root.getChild("events"); eventMap != null; eventMap = eventMap.next) { EventData data = new EventData(eventMap.name); data.intValue = eventMap.getInt("int", 0); data.floatValue = eventMap.getFloat("float", 0f); data.stringValue = eventMap.getString("string", ""); skeletonData.events.add(data); } // Animations. for (JsonValue animationMap = root.getChild("animations"); animationMap != null; animationMap = animationMap.next) { try { readAnimation(animationMap, animationMap.name, skeletonData); } catch (Exception ex) { throw new SerializationException("Error reading animation: " + animationMap.name, ex); } } skeletonData.bones.shrink(); skeletonData.slots.shrink(); skeletonData.skins.shrink(); skeletonData.events.shrink(); skeletonData.animations.shrink(); skeletonData.ikConstraints.shrink(); return skeletonData; } private Attachment readAttachment (JsonValue map, Skin skin, int slotIndex, String name) { float scale = this.scale; name = map.getString("name", name); String type = map.getString("type", AttachmentType.region.name()); switch (AttachmentType.valueOf(type)) { case region: { String path = map.getString("path", name); RegionAttachment region = attachmentLoader.newRegionAttachment(skin, name, path); if (region == null) return null; region.setPath(path); region.setX(map.getFloat("x", 0) * scale); region.setY(map.getFloat("y", 0) * scale); region.setScaleX(map.getFloat("scaleX", 1)); region.setScaleY(map.getFloat("scaleY", 1)); region.setRotation(map.getFloat("rotation", 0)); region.setWidth(map.getFloat("width") * scale); region.setHeight(map.getFloat("height") * scale); String color = map.getString("color", null); if (color != null) region.getColor().set(Color.valueOf(color)); region.updateOffset(); return region; } case boundingbox: { BoundingBoxAttachment box = attachmentLoader.newBoundingBoxAttachment(skin, name); if (box == null) return null; readVertices(map, box, map.getInt("vertexCount") << 1); String color = map.getString("color", null); if (color != null) box.getColor().set(Color.valueOf(color)); return box; } case mesh: case linkedmesh: { String path = map.getString("path", name); MeshAttachment mesh = attachmentLoader.newMeshAttachment(skin, name, path); if (mesh == null) return null; mesh.setPath(path); String color = map.getString("color", null); if (color != null) mesh.getColor().set(Color.valueOf(color)); mesh.setWidth(map.getFloat("width", 0) * scale); mesh.setHeight(map.getFloat("height", 0) * scale); String parent = map.getString("parent", null); if (parent != null) { mesh.setInheritDeform(map.getBoolean("deform", true)); linkedMeshes.add(new LinkedMesh(mesh, map.getString("skin", null), slotIndex, parent)); return mesh; } float[] uvs = map.require("uvs").asFloatArray(); readVertices(map, mesh, uvs.length); mesh.setTriangles(map.require("triangles").asShortArray()); mesh.setRegionUVs(uvs); mesh.updateUVs(); if (map.has("hull")) mesh.setHullLength(map.require("hull").asInt() * 2); if (map.has("edges")) mesh.setEdges(map.require("edges").asShortArray()); return mesh; } case path: { PathAttachment path = attachmentLoader.newPathAttachment(skin, name); if (path == null) return null; path.setClosed(map.getBoolean("closed", false)); path.setConstantSpeed(map.getBoolean("constantSpeed", true)); int vertexCount = map.getInt("vertexCount"); readVertices(map, path, vertexCount << 1); float[] lengths = new float[vertexCount / 3]; int i = 0; for (JsonValue curves = map.require("lengths").child; curves != null; curves = curves.next) lengths[i++] = curves.asFloat() * scale; path.setLengths(lengths); String color = map.getString("color", null); if (color != null) path.getColor().set(Color.valueOf(color)); return path; } } return null; } private void readVertices (JsonValue map, VertexAttachment attachment, int verticesLength) { attachment.setWorldVerticesLength(verticesLength); float[] vertices = map.require("vertices").asFloatArray(); if (verticesLength == vertices.length) { if (scale != 1) { for (int i = 0, n = vertices.length; i < n; i++) vertices[i] *= scale; } attachment.setVertices(vertices); return; } FloatArray weights = new FloatArray(verticesLength * 3 * 3); IntArray bones = new IntArray(verticesLength * 3); for (int i = 0, n = vertices.length; i < n;) { int boneCount = (int)vertices[i++]; bones.add(boneCount); for (int nn = i + boneCount * 4; i < nn; i += 4) { bones.add((int)vertices[i]); weights.add(vertices[i + 1] * scale); weights.add(vertices[i + 2] * scale); weights.add(vertices[i + 3]); } } attachment.setBones(bones.toArray()); attachment.setVertices(weights.toArray()); } private void readAnimation (JsonValue map, String name, SkeletonData skeletonData) { float scale = this.scale; Array<Timeline> timelines = new Array(); float duration = 0; // Slot timelines. for (JsonValue slotMap = map.getChild("slots"); slotMap != null; slotMap = slotMap.next) { SlotData slot = skeletonData.findSlot(slotMap.name); if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name); for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) { String timelineName = timelineMap.name; if (timelineName.equals("color")) { ColorTimeline timeline = new ColorTimeline(timelineMap.size); timeline.slotIndex = slot.index; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) { Color color = Color.valueOf(valueMap.getString("color")); timeline.setFrame(frameIndex, valueMap.getFloat("time"), color.r, color.g, color.b, color.a); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * ColorTimeline.ENTRIES]); } else if (timelineName.equals("attachment")) { AttachmentTimeline timeline = new AttachmentTimeline(timelineMap.size); timeline.slotIndex = slot.index; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) timeline.setFrame(frameIndex++, valueMap.getFloat("time"), valueMap.getString("name")); timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]); } else throw new RuntimeException("Invalid timeline type for a slot: " + timelineName + " (" + slotMap.name + ")"); } } // Bone timelines. for (JsonValue boneMap = map.getChild("bones"); boneMap != null; boneMap = boneMap.next) { BoneData bone = skeletonData.findBone(boneMap.name); if (bone == null) throw new SerializationException("Bone not found: " + boneMap.name); for (JsonValue timelineMap = boneMap.child; timelineMap != null; timelineMap = timelineMap.next) { String timelineName = timelineMap.name; if (timelineName.equals("rotate")) { RotateTimeline timeline = new RotateTimeline(timelineMap.size); timeline.boneIndex = bone.index; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) { timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("angle")); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * RotateTimeline.ENTRIES]); } else if (timelineName.equals("translate") || timelineName.equals("scale") || timelineName.equals("shear")) { TranslateTimeline timeline; float timelineScale = 1; if (timelineName.equals("scale")) timeline = new ScaleTimeline(timelineMap.size); else if (timelineName.equals("shear")) timeline = new ShearTimeline(timelineMap.size); else { timeline = new TranslateTimeline(timelineMap.size); timelineScale = scale; } timeline.boneIndex = bone.index; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) { float x = valueMap.getFloat("x", 0), y = valueMap.getFloat("y", 0); timeline.setFrame(frameIndex, valueMap.getFloat("time"), x * timelineScale, y * timelineScale); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * TranslateTimeline.ENTRIES]); } else throw new RuntimeException("Invalid timeline type for a bone: " + timelineName + " (" + boneMap.name + ")"); } } // IK constraint timelines. for (JsonValue constraintMap = map.getChild("ik"); constraintMap != null; constraintMap = constraintMap.next) { IkConstraintData constraint = skeletonData.findIkConstraint(constraintMap.name); IkConstraintTimeline timeline = new IkConstraintTimeline(constraintMap.size); timeline.ikConstraintIndex = skeletonData.getIkConstraints().indexOf(constraint, true); int frameIndex = 0; for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) { timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("mix", 1), valueMap.getBoolean("bendPositive", true) ? 1 : -1); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * IkConstraintTimeline.ENTRIES]); } // Transform constraint timelines. for (JsonValue constraintMap = map.getChild("transform"); constraintMap != null; constraintMap = constraintMap.next) { TransformConstraintData constraint = skeletonData.findTransformConstraint(constraintMap.name); TransformConstraintTimeline timeline = new TransformConstraintTimeline(constraintMap.size); timeline.transformConstraintIndex = skeletonData.getTransformConstraints().indexOf(constraint, true); int frameIndex = 0; for (JsonValue valueMap = constraintMap.child; valueMap != null; valueMap = valueMap.next) { timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1), valueMap.getFloat("translateMix", 1), valueMap.getFloat("scaleMix", 1), valueMap.getFloat("shearMix", 1)); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * TransformConstraintTimeline.ENTRIES]); } // Path constraint timelines. for (JsonValue constraintMap = map.getChild("paths"); constraintMap != null; constraintMap = constraintMap.next) { PathConstraintData data = skeletonData.findPathConstraint(constraintMap.name); if (data == null) throw new SerializationException("Path constraint not found: " + constraintMap.name); int index = skeletonData.pathConstraints.indexOf(data, true); for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) { String timelineName = timelineMap.name; if (timelineName.equals("position") || timelineName.equals("spacing")) { PathConstraintPositionTimeline timeline; float timelineScale = 1; if (timelineName.equals("spacing")) { timeline = new PathConstraintSpacingTimeline(timelineMap.size); if (data.spacingMode == SpacingMode.length || data.spacingMode == SpacingMode.fixed) timelineScale = scale; } else { timeline = new PathConstraintPositionTimeline(timelineMap.size); if (data.positionMode == PositionMode.fixed) timelineScale = scale; } timeline.pathConstraintIndex = index; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) { timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat(timelineName, 0) * timelineScale); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintPositionTimeline.ENTRIES]); } else if (timelineName.equals("mix")) { PathConstraintMixTimeline timeline = new PathConstraintMixTimeline(timelineMap.size); timeline.pathConstraintIndex = index; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) { timeline.setFrame(frameIndex, valueMap.getFloat("time"), valueMap.getFloat("rotateMix", 1), valueMap.getFloat("translateMix", 1)); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[(timeline.getFrameCount() - 1) * PathConstraintMixTimeline.ENTRIES]); } } } // Deform timelines. for (JsonValue deformMap = map.getChild("deform"); deformMap != null; deformMap = deformMap.next) { Skin skin = skeletonData.findSkin(deformMap.name); if (skin == null) throw new SerializationException("Skin not found: " + deformMap.name); for (JsonValue slotMap = deformMap.child; slotMap != null; slotMap = slotMap.next) { SlotData slot = skeletonData.findSlot(slotMap.name); if (slot == null) throw new SerializationException("Slot not found: " + slotMap.name); for (JsonValue timelineMap = slotMap.child; timelineMap != null; timelineMap = timelineMap.next) { VertexAttachment attachment = (VertexAttachment)skin.getAttachment(slot.index, timelineMap.name); if (attachment == null) throw new SerializationException("Deform attachment not found: " + timelineMap.name); boolean weighted = attachment.getBones() != null; float[] vertices = attachment.getVertices(); int deformLength = weighted ? vertices.length / 3 * 2 : vertices.length; DeformTimeline timeline = new DeformTimeline(timelineMap.size); timeline.slotIndex = slot.index; timeline.attachment = attachment; int frameIndex = 0; for (JsonValue valueMap = timelineMap.child; valueMap != null; valueMap = valueMap.next) { float[] deform; JsonValue verticesValue = valueMap.get("vertices"); if (verticesValue == null) deform = weighted ? new float[deformLength] : vertices; else { deform = new float[deformLength]; int start = valueMap.getInt("offset", 0); System.arraycopy(verticesValue.asFloatArray(), 0, deform, start, verticesValue.size); if (scale != 1) { for (int i = start, n = i + verticesValue.size; i < n; i++) deform[i] *= scale; } if (!weighted) { for (int i = 0; i < deformLength; i++) deform[i] += vertices[i]; } } timeline.setFrame(frameIndex, valueMap.getFloat("time"), deform); readCurve(valueMap, timeline, frameIndex); frameIndex++; } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]); } } } // Draw order timeline. JsonValue drawOrdersMap = map.get("drawOrder"); if (drawOrdersMap == null) drawOrdersMap = map.get("draworder"); if (drawOrdersMap != null) { DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrdersMap.size); int slotCount = skeletonData.slots.size; int frameIndex = 0; for (JsonValue drawOrderMap = drawOrdersMap.child; drawOrderMap != null; drawOrderMap = drawOrderMap.next) { int[] drawOrder = null; JsonValue offsets = drawOrderMap.get("offsets"); if (offsets != null) { drawOrder = new int[slotCount]; for (int i = slotCount - 1; i >= 0; i--) drawOrder[i] = -1; int[] unchanged = new int[slotCount - offsets.size]; int originalIndex = 0, unchangedIndex = 0; for (JsonValue offsetMap = offsets.child; offsetMap != null; offsetMap = offsetMap.next) { SlotData slot = skeletonData.findSlot(offsetMap.getString("slot")); if (slot == null) throw new SerializationException("Slot not found: " + offsetMap.getString("slot")); // Collect unchanged items. while (originalIndex != slot.index) unchanged[unchangedIndex++] = originalIndex++; // Set changed items. drawOrder[originalIndex + offsetMap.getInt("offset")] = originalIndex++; } // Collect remaining unchanged items. while (originalIndex < slotCount) unchanged[unchangedIndex++] = originalIndex++; // Fill in unchanged items. for (int i = slotCount - 1; i >= 0; i--) if (drawOrder[i] == -1) drawOrder[i] = unchanged[--unchangedIndex]; } timeline.setFrame(frameIndex++, drawOrderMap.getFloat("time"), drawOrder); } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]); } // Event timeline. JsonValue eventsMap = map.get("events"); if (eventsMap != null) { EventTimeline timeline = new EventTimeline(eventsMap.size); int frameIndex = 0; for (JsonValue eventMap = eventsMap.child; eventMap != null; eventMap = eventMap.next) { EventData eventData = skeletonData.findEvent(eventMap.getString("name")); if (eventData == null) throw new SerializationException("Event not found: " + eventMap.getString("name")); Event event = new Event(eventMap.getFloat("time"), eventData); event.intValue = eventMap.getInt("int", eventData.getInt()); event.floatValue = eventMap.getFloat("float", eventData.getFloat()); event.stringValue = eventMap.getString("string", eventData.getString()); timeline.setFrame(frameIndex++, event); } timelines.add(timeline); duration = Math.max(duration, timeline.getFrames()[timeline.getFrameCount() - 1]); } timelines.shrink(); skeletonData.animations.add(new Animation(name, timelines, duration)); } void readCurve (JsonValue map, CurveTimeline timeline, int frameIndex) { JsonValue curve = map.get("curve"); if (curve == null) return; if (curve.isString() && curve.asString().equals("stepped")) timeline.setStepped(frameIndex); else if (curve.isArray()) { timeline.setCurve(frameIndex, curve.getFloat(0), curve.getFloat(1), curve.getFloat(2), curve.getFloat(3)); } } static class LinkedMesh { String parent, skin; int slotIndex; MeshAttachment mesh; public LinkedMesh (MeshAttachment mesh, String skin, int slotIndex, String parent) { this.mesh = mesh; this.skin = skin; this.slotIndex = slotIndex; this.parent = parent; } } }