From f0c61be159f24e6f75ab1af234b6ec3f3fe2b921 Mon Sep 17 00:00:00 2001 From: Nathan Sweet Date: Mon, 21 Apr 2025 17:21:18 -0400 Subject: [PATCH] [libgdx] Added slider mix timeline, JSON/binary fixes. --- .../com/esotericsoftware/spine/Animation.java | 222 ++++++++++-------- .../spine/SkeletonBinary.java | 74 ++++-- .../esotericsoftware/spine/SkeletonJson.java | 65 ++++- 3 files changed, 229 insertions(+), 132 deletions(-) diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java index 8afa3318c..52e9b90ac 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/Animation.java @@ -182,7 +182,8 @@ public class Animation { pathConstraintPosition, pathConstraintSpacing, pathConstraintMix, // physicsConstraintInertia, physicsConstraintStrength, physicsConstraintDamping, physicsConstraintMass, // physicsConstraintWind, physicsConstraintGravity, physicsConstraintMix, physicsConstraintReset, // - sequence + sequence, // + sliderMix } /** The base class for all timelines. */ @@ -1711,6 +1712,107 @@ public class Animation { } } + /** Changes a slot's {@link SlotPose#getSequenceIndex()} for an attachment's {@link Sequence}. */ + static public class SequenceTimeline extends Timeline implements SlotTimeline { + static public final int ENTRIES = 3; + static private final int MODE = 1, DELAY = 2; + + final int slotIndex; + final HasTextureRegion attachment; + + public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) { + super(frameCount, + Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasTextureRegion)attachment).getSequence().getId()); + this.slotIndex = slotIndex; + this.attachment = (HasTextureRegion)attachment; + } + + public int getFrameEntries () { + return ENTRIES; + } + + public int getSlotIndex () { + return slotIndex; + } + + public Attachment getAttachment () { + return (Attachment)attachment; + } + + /** Sets the time, mode, index, and frame time for the specified frame. + * @param frame Between 0 and frameCount, inclusive. + * @param time Seconds between frames. */ + public void setFrame (int frame, float time, SequenceMode mode, int index, float delay) { + frame *= ENTRIES; + frames[frame] = time; + frames[frame + MODE] = mode.ordinal() | (index << 4); + frames[frame + DELAY] = delay; + } + + public void apply (Skeleton skeleton, float lastTime, float time, @Null Array events, float alpha, MixBlend blend, + MixDirection direction, boolean appliedPose) { + + Slot slot = skeleton.slots.items[slotIndex]; + if (!slot.bone.active) return; + SlotPose pose = appliedPose ? slot.applied : slot.pose; + + Attachment slotAttachment = pose.attachment; + if (slotAttachment != attachment) { + if (!(slotAttachment instanceof VertexAttachment vertexAttachment) + || vertexAttachment.getTimelineAttachment() != attachment) return; + } + Sequence sequence = ((HasTextureRegion)slotAttachment).getSequence(); + if (sequence == null) return; + + if (direction == out) { + if (blend == setup) pose.setSequenceIndex(-1); + return; + } + + float[] frames = this.frames; + if (time < frames[0]) { + if (blend == setup || blend == first) pose.setSequenceIndex(-1); + return; + } + + int i = search(frames, time, ENTRIES); + float before = frames[i]; + int modeAndIndex = (int)frames[i + MODE]; + float delay = frames[i + DELAY]; + + int index = modeAndIndex >> 4, count = sequence.getRegions().length; + SequenceMode mode = SequenceMode.values[modeAndIndex & 0xf]; + if (mode != SequenceMode.hold) { + index += (time - before) / delay + 0.0001f; + switch (mode) { + case once: + index = Math.min(count - 1, index); + break; + case loop: + index %= count; + break; + case pingpong: { + int n = (count << 1) - 2; + index = n == 0 ? 0 : index % n; + if (index >= count) index = n - index; + break; + } + case onceReverse: + index = Math.max(count - 1 - index, 0); + break; + case loopReverse: + index = count - 1 - (index % count); + break; + case pingpongReverse: + int n = (count << 1) - 2; + index = n == 0 ? 0 : (index + count - 1) % n; + if (index >= count) index = n - index; + } + } + pose.setSequenceIndex(index); + } + } + /** Fires an {@link Event} when specific animation times are reached. */ static public class EventTimeline extends Timeline { static private final String[] propertyIds = {Integer.toString(Property.event.ordinal())}; @@ -1827,6 +1929,7 @@ public class Animation { } static public interface ConstraintTimeline { + /** The index of the constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is applied. */ public int getConstraintIndex (); } @@ -1848,8 +1951,6 @@ public class Animation { return ENTRIES; } - /** The index of the IK constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is - * applied. */ public int getConstraintIndex () { return constraintIndex; } @@ -1960,8 +2061,6 @@ public class Animation { return ENTRIES; } - /** The index of the transform constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is - * applied. */ public int getConstraintIndex () { return constraintIndex; } @@ -2075,8 +2174,6 @@ public class Animation { this.constraintIndex = constraintIndex; } - /** The index of the path constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is - * applied. */ public int getConstraintIndex () { return constraintIndex; } @@ -2101,8 +2198,6 @@ public class Animation { this.constraintIndex = constraintIndex; } - /** The index of the path constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is - * applied. */ public int getConstraintIndex () { return constraintIndex; } @@ -2135,8 +2230,6 @@ public class Animation { return ENTRIES; } - /** The index of the path constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is - * applied. */ public int getConstraintIndex () { return constraintIndex; } @@ -2223,8 +2316,8 @@ public class Animation { this.constraintIndex = constraintIndex; } - /** The index of the physics constraint in {@link Skeleton#getPhysicsConstraints()} that will be changed when this timeline - * is applied, or -1 if all physics constraints in the skeleton will be changed. */ + /** The index of the physics constraint in {@link Skeleton#getConstraints()} that will be changed when this timeline is + * applied, or -1 if all physics constraints in the skeleton will be changed. */ public int getConstraintIndex () { return constraintIndex; } @@ -2404,7 +2497,7 @@ public class Animation { this.constraintIndex = constraintIndex; } - /** The index of the physics constraint in {@link Skeleton#getPhysicsConstraints()} that will be reset when this timeline is + /** The index of the physics constraint in {@link Skeleton#getConstraints()} that will be reset when this timeline is * applied, or -1 if all physics constraints in the skeleton will be reset. */ public int getConstraintIndex () { return constraintIndex; @@ -2453,104 +2546,27 @@ public class Animation { } } - /** Changes a slot's {@link SlotPose#getSequenceIndex()} for an attachment's {@link Sequence}. */ - static public class SequenceTimeline extends Timeline implements SlotTimeline { - static public final int ENTRIES = 3; - static private final int MODE = 1, DELAY = 2; + /** Changes a slider's {@link SliderPose#getMix()}. */ + static public class SliderMixTimeline extends CurveTimeline1 implements ConstraintTimeline { + final int constraintIndex; - final int slotIndex; - final HasTextureRegion attachment; - - public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) { - super(frameCount, - Property.sequence.ordinal() + "|" + slotIndex + "|" + ((HasTextureRegion)attachment).getSequence().getId()); - this.slotIndex = slotIndex; - this.attachment = (HasTextureRegion)attachment; + public SliderMixTimeline (int frameCount, int bezierCount, int constraintIndex) { + super(frameCount, bezierCount, Property.sliderMix.ordinal() + "|" + constraintIndex); + this.constraintIndex = constraintIndex; } - public int getFrameEntries () { - return ENTRIES; - } - - public int getSlotIndex () { - return slotIndex; - } - - public Attachment getAttachment () { - return (Attachment)attachment; - } - - /** Sets the time, mode, index, and frame time for the specified frame. - * @param frame Between 0 and frameCount, inclusive. - * @param time Seconds between frames. */ - public void setFrame (int frame, float time, SequenceMode mode, int index, float delay) { - frame *= ENTRIES; - frames[frame] = time; - frames[frame + MODE] = mode.ordinal() | (index << 4); - frames[frame + DELAY] = delay; + public int getConstraintIndex () { + return constraintIndex; } public void apply (Skeleton skeleton, float lastTime, float time, @Null Array events, float alpha, MixBlend blend, MixDirection direction, boolean appliedPose) { - Slot slot = skeleton.slots.items[slotIndex]; - if (!slot.bone.active) return; - SlotPose pose = appliedPose ? slot.applied : slot.pose; - - Attachment slotAttachment = pose.attachment; - if (slotAttachment != attachment) { - if (!(slotAttachment instanceof VertexAttachment vertexAttachment) - || vertexAttachment.getTimelineAttachment() != attachment) return; + var constraint = (Slider)skeleton.constraints.items[constraintIndex]; + if (constraint.active) { + SliderPose pose = appliedPose ? constraint.applied : constraint.pose; + pose.mix = getAbsoluteValue(time, alpha, blend, pose.mix, constraint.data.setup.mix); } - Sequence sequence = ((HasTextureRegion)slotAttachment).getSequence(); - if (sequence == null) return; - - if (direction == out) { - if (blend == setup) pose.setSequenceIndex(-1); - return; - } - - float[] frames = this.frames; - if (time < frames[0]) { - if (blend == setup || blend == first) pose.setSequenceIndex(-1); - return; - } - - int i = search(frames, time, ENTRIES); - float before = frames[i]; - int modeAndIndex = (int)frames[i + MODE]; - float delay = frames[i + DELAY]; - - int index = modeAndIndex >> 4, count = sequence.getRegions().length; - SequenceMode mode = SequenceMode.values[modeAndIndex & 0xf]; - if (mode != SequenceMode.hold) { - index += (time - before) / delay + 0.0001f; - switch (mode) { - case once: - index = Math.min(count - 1, index); - break; - case loop: - index %= count; - break; - case pingpong: { - int n = (count << 1) - 2; - index = n == 0 ? 0 : index % n; - if (index >= count) index = n - index; - break; - } - case onceReverse: - index = Math.max(count - 1 - index, 0); - break; - case loopReverse: - index = count - 1 - (index % count); - break; - case pingpongReverse: - int n = (count << 1) - 2; - index = n == 0 ? 0 : (index + count - 1) % n; - if (index >= count) index = n - index; - } - } - pose.setSequenceIndex(index); } } } diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java index 6eedba5d9..f7bb332f7 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonBinary.java @@ -76,6 +76,7 @@ import com.esotericsoftware.spine.Animation.SequenceTimeline; import com.esotericsoftware.spine.Animation.ShearTimeline; import com.esotericsoftware.spine.Animation.ShearXTimeline; import com.esotericsoftware.spine.Animation.ShearYTimeline; +import com.esotericsoftware.spine.Animation.SliderMixTimeline; import com.esotericsoftware.spine.Animation.Timeline; import com.esotericsoftware.spine.Animation.TransformConstraintTimeline; import com.esotericsoftware.spine.Animation.TranslateTimeline; @@ -141,6 +142,7 @@ public class SkeletonBinary extends SkeletonLoader { static public final int CONSTRAINT_PATH = 1; static public final int CONSTRAINT_TRANSFORM = 2; static public final int CONSTRAINT_PHYSICS = 3; + static public final int CONSTRAINT_SLIDER = 4; static public final int ATTACHMENT_DEFORM = 0; static public final int ATTACHMENT_SEQUENCE = 1; @@ -158,6 +160,8 @@ public class SkeletonBinary extends SkeletonLoader { static public final int PHYSICS_MIX = 7; static public final int PHYSICS_RESET = 8; + static public final int SLIDER_MIX = 0; + static public final int CURVE_LINEAR = 0; static public final int CURVE_STEPPED = 1; static public final int CURVE_BEZIER = 2; @@ -259,12 +263,15 @@ public class SkeletonBinary extends SkeletonLoader { } // Constraints. - o = skeletonData.constraints.setSize(n = input.readInt(true)); - for (int i = 0, nn; i < n; i++) { + int constraintCount = input.readInt(true); + ConstraintData[] constraints = skeletonData.constraints.setSize(constraintCount); + for (int i = 0; i < constraintCount; i++) { + String name = input.readString(); + int nn = input.readInt(true); switch (input.readByte()) { case CONSTRAINT_IK -> { - var data = new IkConstraintData(input.readString()); - BoneData[] constraintBones = data.bones.setSize(nn = input.readInt(true)); + var data = new IkConstraintData(name); + BoneData[] constraintBones = data.bones.setSize(nn); for (int ii = 0; ii < nn; ii++) constraintBones[ii] = bones[input.readInt(true)]; data.target = bones[input.readInt(true)]; @@ -277,11 +284,11 @@ public class SkeletonBinary extends SkeletonLoader { 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; - o[i] = data; + constraints[i] = data; } case CONSTRAINT_TRANSFORM -> { - var data = new TransformConstraintData(input.readString()); - BoneData[] constraintBones = data.bones.setSize(nn = input.readInt(true)); + var data = new TransformConstraintData(name); + BoneData[] constraintBones = data.bones.setSize(nn); for (int ii = 0; ii < nn; ii++) constraintBones[ii] = bones[input.readInt(true)]; data.source = bones[input.readInt(true)]; @@ -352,11 +359,11 @@ public class SkeletonBinary extends SkeletonLoader { if ((flags & 8) != 0) setup.mixScaleX = input.readFloat(); if ((flags & 16) != 0) setup.mixScaleY = input.readFloat(); if ((flags & 32) != 0) setup.mixShearY = input.readFloat(); - o[i] = data; + constraints[i] = data; } case CONSTRAINT_PATH -> { - var data = new PathConstraintData(input.readString()); - BoneData[] constraintBones = data.bones.setSize(nn = input.readInt(true)); + var data = new PathConstraintData(name); + BoneData[] constraintBones = data.bones.setSize(nn); for (int ii = 0; ii < nn; ii++) constraintBones[ii] = bones[input.readInt(true)]; data.slot = slots[input.readInt(true)]; @@ -374,11 +381,11 @@ public class SkeletonBinary extends SkeletonLoader { setup.mixRotate = input.readFloat(); setup.mixX = input.readFloat(); setup.mixY = input.readFloat(); - o[i] = data; + constraints[i] = data; } case CONSTRAINT_PHYSICS -> { - var data = new PhysicsConstraintData(input.readString()); - data.bone = bones[input.readInt(true)]; + var data = new PhysicsConstraintData(name); + data.bone = bones[nn]; int flags = input.read(); data.skinRequired = (flags & 1) != 0; if ((flags & 2) != 0) data.x = input.readFloat(); @@ -404,7 +411,14 @@ public class SkeletonBinary extends SkeletonLoader { if ((flags & 32) != 0) data.gravityGlobal = true; if ((flags & 64) != 0) data.mixGlobal = true; setup.mix = (flags & 128) != 0 ? input.readFloat() : 1; - o[i] = data; + constraints[i] = data; + } + case CONSTRAINT_SLIDER -> { + var data = new SliderData(name); + data.skinRequired = (nn & 1) != 0; + if ((nn & 2) != 0) data.setup.mix = (nn & 4) != 0 ? input.readFloat() : 1; + if ((nn & 8) != 0) data.setup.time = input.readFloat(); + constraints[i] = data; } } } @@ -454,10 +468,12 @@ public class SkeletonBinary extends SkeletonLoader { } // Animations. - o = skeletonData.animations.setSize(n = input.readInt(true)); + Animation[] animations = skeletonData.animations.setSize(n = input.readInt(true)); for (int i = 0; i < n; i++) - o[i] = readAnimation(input, input.readString(), skeletonData); + animations[i] = readAnimation(input, input.readString(), skeletonData); + for (int i = 0; i < constraintCount; i++) + if (constraints[i] instanceof SliderData data) data.animation = animations[input.readInt(true)]; } catch (IOException ex) { throw new SerializationException("Error reading skeleton file.", ex); } finally { @@ -1034,6 +1050,32 @@ public class SkeletonBinary extends SkeletonLoader { } } + // Slider timelines. + for (int i = 0, n = input.readInt(true); i < n; i++) { + int index = input.readInt(true); + for (int ii = 0, nn = input.readInt(true); ii < nn; ii++) { + int type = input.readByte(), frameCount = input.readInt(true), bezierCount = input.readInt(true); + switch (type) { + case SLIDER_MIX -> { + var timeline = new SliderMixTimeline(frameCount, bezierCount, index); + float time = input.readFloat(), mix = input.readFloat(); + for (int frame = 0, bezier = 0, frameLast = timeline.getFrameCount() - 1;; frame++) { + timeline.setFrame(frame, time, mix); + if (frame == frameLast) break; + float time2 = input.readFloat(), mix2 = input.readFloat(); + switch (input.readByte()) { + case CURVE_STEPPED -> timeline.setStepped(frame); + case CURVE_BEZIER -> setBezier(input, timeline, bezier++, frame, 0, time, time2, mix, mix2, 1); + } + time = time2; + mix = mix2; + } + timelines.add(timeline); + } + } + } + } + // Attachment timelines. for (int i = 0, n = input.readInt(true); i < n; i++) { Skin skin = skeletonData.skins.items[input.readInt(true)]; diff --git a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java index f93fc24f6..f85def428 100644 --- a/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java +++ b/spine-libgdx/spine-libgdx/src/com/esotericsoftware/spine/SkeletonJson.java @@ -77,6 +77,7 @@ import com.esotericsoftware.spine.Animation.SequenceTimeline; import com.esotericsoftware.spine.Animation.ShearTimeline; import com.esotericsoftware.spine.Animation.ShearXTimeline; import com.esotericsoftware.spine.Animation.ShearYTimeline; +import com.esotericsoftware.spine.Animation.SliderMixTimeline; import com.esotericsoftware.spine.Animation.Timeline; import com.esotericsoftware.spine.Animation.TransformConstraintTimeline; import com.esotericsoftware.spine.Animation.TranslateTimeline; @@ -217,10 +218,12 @@ public class SkeletonJson extends SkeletonLoader { // Constraints. for (JsonValue constraintMap = root.getChild("constraints"); constraintMap != null; constraintMap = constraintMap.next) { + String name = constraintMap.getString("name"); + boolean skinRequired = constraintMap.getBoolean("skin", false); switch (constraintMap.getString("type")) { case "ik" -> { - var data = new IkConstraintData(constraintMap.getString("name")); - data.skinRequired = constraintMap.getBoolean("skin", false); + var data = new IkConstraintData(name); + data.skinRequired = skinRequired; for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) { BoneData bone = skeletonData.findBone(entry.asString()); @@ -243,8 +246,8 @@ public class SkeletonJson extends SkeletonLoader { skeletonData.constraints.add(data); } case "transform" -> { - var data = new TransformConstraintData(constraintMap.getString("name")); - data.skinRequired = constraintMap.getBoolean("skin", false); + var data = new TransformConstraintData(name); + data.skinRequired = skinRequired; for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) { BoneData bone = skeletonData.findBone(entry.asString()); @@ -340,8 +343,8 @@ public class SkeletonJson extends SkeletonLoader { skeletonData.constraints.add(data); } case "path" -> { - var data = new PathConstraintData(constraintMap.getString("name")); - data.skinRequired = constraintMap.getBoolean("skin", false); + var data = new PathConstraintData(name); + data.skinRequired = skinRequired; for (JsonValue entry = constraintMap.getChild("bones"); entry != null; entry = entry.next) { BoneData bone = skeletonData.findBone(entry.asString()); @@ -369,8 +372,8 @@ public class SkeletonJson extends SkeletonLoader { skeletonData.constraints.add(data); } case "physics" -> { - var data = new PhysicsConstraintData(constraintMap.getString("name")); - data.skinRequired = constraintMap.getBoolean("skin", false); + var data = new PhysicsConstraintData(name); + data.skinRequired = skinRequired; String boneName = constraintMap.getString("bone"); data.bone = skeletonData.findBone(boneName); @@ -384,9 +387,9 @@ public class SkeletonJson extends SkeletonLoader { data.limit = constraintMap.getFloat("limit", 5000) * scale; data.step = 1f / constraintMap.getInt("fps", 60); PhysicsConstraintPose setup = data.setup; - setup.inertia = constraintMap.getFloat("inertia", 1); + setup.inertia = constraintMap.getFloat("inertia", 0.5f); setup.strength = constraintMap.getFloat("strength", 100); - setup.damping = constraintMap.getFloat("damping", 1); + setup.damping = constraintMap.getFloat("damping", 0.85f); setup.massInverse = 1 / constraintMap.getFloat("mass", 1); setup.wind = constraintMap.getFloat("wind", 0); setup.gravity = constraintMap.getFloat("gravity", 0); @@ -401,7 +404,13 @@ public class SkeletonJson extends SkeletonLoader { skeletonData.constraints.add(data); } - // BOZO! - Sliders. + case "slider" -> { + var data = new SliderData(name); + data.skinRequired = skinRequired; + data.setup.time = constraintMap.getFloat("time", 0); + data.setup.mix = constraintMap.getFloat("mix", 1); + skeletonData.constraints.add(data); + } } } @@ -492,6 +501,16 @@ public class SkeletonJson extends SkeletonLoader { } } + // Slider animations. + for (JsonValue constraintMap = root.getChild("constraints"); constraintMap != null; constraintMap = constraintMap.next) { + if (constraintMap.getString("type").equals("slider")) { + SliderData data = skeletonData.findConstraint(constraintMap.getString("name"), SliderData.class); + String animationName = constraintMap.getString("animation"); + data.animation = skeletonData.findAnimation(animationName); + if (data.animation == null) throw new SerializationException("Slider animation not found: " + animationName); + } + } + skeletonData.bones.shrink(); skeletonData.slots.shrink(); skeletonData.skins.shrink(); @@ -1032,6 +1051,7 @@ public class SkeletonJson extends SkeletonLoader { int frames = timelineMap.size; CurveTimeline1 timeline; + float defaultValue = 0; switch (timelineMap.name) { case "reset" -> { var resetTimeline = new PhysicsConstraintResetTimeline(frames, index); @@ -1046,12 +1066,31 @@ public class SkeletonJson extends SkeletonLoader { case "mass" -> timeline = new PhysicsConstraintMassTimeline(frames, frames, index); case "wind" -> timeline = new PhysicsConstraintWindTimeline(frames, frames, index); case "gravity" -> timeline = new PhysicsConstraintGravityTimeline(frames, frames, index); - case "mix" -> timeline = new PhysicsConstraintMixTimeline(frames, frames, index); + case "mix" -> { + defaultValue = 1; + timeline = new PhysicsConstraintMixTimeline(frames, frames, index); + } default -> { continue; } } - timelines.add(readTimeline(keyMap, timeline, 0, 1)); + timelines.add(readTimeline(keyMap, timeline, defaultValue, 1)); + } + } + + // Slider timelines. + for (JsonValue constraintMap = map.getChild("slider"); constraintMap != null; constraintMap = constraintMap.next) { + SliderData constraint = skeletonData.findConstraint(constraintMap.name, SliderData.class); + if (constraint == null) throw new SerializationException("Slider not found: " + constraintMap.name); + int index = skeletonData.constraints.indexOf(constraint, true); + for (JsonValue timelineMap = constraintMap.child; timelineMap != null; timelineMap = timelineMap.next) { + JsonValue keyMap = timelineMap.child; + if (keyMap == null) continue; + + int frames = timelineMap.size; + switch (timelineMap.name) { + case "mix" -> timelines.add(readTimeline(keyMap, new SliderMixTimeline(frames, frames, index), 1, 1)); + } } }