diff --git a/SpineRuntimes/SpineRuntime21/Animation.cs b/SpineRuntimes/SpineRuntime21/Animation.cs new file mode 100644 index 0000000..301271a --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Animation.cs @@ -0,0 +1,723 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class Animation { + internal List timelines; + internal float duration; + internal String name; + + public String Name { get { return name; } } + public List Timelines { get { return timelines; } set { timelines = value; } } + public float Duration { get { return duration; } set { duration = value; } } + + public Animation (String name, List timelines, float duration) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + if (timelines == null) throw new ArgumentNullException("timelines cannot be null."); + this.name = name; + this.timelines = timelines; + this.duration = duration; + } + + /// Poses the skeleton at the specified time for this animation. + /// The last time the animation was applied. + /// Any triggered events are added. + public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, List events) { + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + + if (loop && duration != 0) { + time %= duration; + lastTime %= duration; + } + + List timelines = this.timelines; + for (int i = 0, n = timelines.Count; i < n; i++) + timelines[i].Apply(skeleton, lastTime, time, events, 1); + } + + /// Poses the skeleton at the specified time for this animation mixed with the current pose. + /// The last time the animation was applied. + /// Any triggered events are added. + /// The amount of this animation that affects the current pose. + public void Mix (Skeleton skeleton, float lastTime, float time, bool loop, List events, float alpha) { + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + + if (loop && duration != 0) { + time %= duration; + lastTime %= duration; + } + + List timelines = this.timelines; + for (int i = 0, n = timelines.Count; i < n; i++) + timelines[i].Apply(skeleton, lastTime, time, events, alpha); + } + + /// After the first and before the last entry. + internal static int binarySearch (float[] values, float target, int step) { + int low = 0; + int high = values.Length / step - 2; + if (high == 0) return step; + int current = (int)((uint)high >> 1); + while (true) { + if (values[(current + 1) * step] <= target) + low = current + 1; + else + high = current; + if (low == high) return (low + 1) * step; + current = (int)((uint)(low + high) >> 1); + } + } + + /// After the first and before the last entry. + internal static int binarySearch (float[] values, float target) { + int low = 0; + int high = values.Length - 2; + if (high == 0) return 1; + int current = (int)((uint)high >> 1); + while (true) { + if (values[(current + 1)] <= target) + low = current + 1; + else + high = current; + if (low == high) return (low + 1); + current = (int)((uint)(low + high) >> 1); + } + } + + internal static int linearSearch (float[] values, float target, int step) { + for (int i = 0, last = values.Length - step; i <= last; i += step) + if (values[i] > target) return i; + return -1; + } + } + + public interface Timeline { + /// Sets the value(s) for the specified time. + /// May be null to not collect fired events. + void Apply (Skeleton skeleton, float lastTime, float time, List events, float alpha); + } + + /// Base class for frames that use an interpolation bezier curve. + abstract public class CurveTimeline : Timeline { + protected const float LINEAR = 0, STEPPED = 1, BEZIER = 2; + protected const int BEZIER_SEGMENTS = 10, BEZIER_SIZE = BEZIER_SEGMENTS * 2 - 1; + + private float[] curves; // type, x, y, ... + public int FrameCount { get { return curves.Length / BEZIER_SIZE + 1; } } + + public CurveTimeline (int frameCount) { + curves = new float[(frameCount - 1) * BEZIER_SIZE]; + } + + abstract public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha); + + public void SetLinear (int frameIndex) { + curves[frameIndex * BEZIER_SIZE] = LINEAR; + } + + public void SetStepped (int frameIndex) { + curves[frameIndex * BEZIER_SIZE] = STEPPED; + } + + /// Sets the control handle positions for an interpolation bezier curve used to transition from this keyframe to the next. + /// cx1 and cx2 are from 0 to 1, representing the percent of time between the two keyframes. cy1 and cy2 are the percent of + /// the difference between the keyframe's values. + public void SetCurve (int frameIndex, float cx1, float cy1, float cx2, float cy2) { + float subdiv1 = 1f / BEZIER_SEGMENTS, subdiv2 = subdiv1 * subdiv1, subdiv3 = subdiv2 * subdiv1; + float pre1 = 3 * subdiv1, pre2 = 3 * subdiv2, pre4 = 6 * subdiv2, pre5 = 6 * subdiv3; + float tmp1x = -cx1 * 2 + cx2, tmp1y = -cy1 * 2 + cy2, tmp2x = (cx1 - cx2) * 3 + 1, tmp2y = (cy1 - cy2) * 3 + 1; + float dfx = cx1 * pre1 + tmp1x * pre2 + tmp2x * subdiv3, dfy = cy1 * pre1 + tmp1y * pre2 + tmp2y * subdiv3; + float ddfx = tmp1x * pre4 + tmp2x * pre5, ddfy = tmp1y * pre4 + tmp2y * pre5; + float dddfx = tmp2x * pre5, dddfy = tmp2y * pre5; + + int i = frameIndex * BEZIER_SIZE; + float[] curves = this.curves; + curves[i++] = BEZIER; + + float x = dfx, y = dfy; + for (int n = i + BEZIER_SIZE - 1; i < n; i += 2) { + curves[i] = x; + curves[i + 1] = y; + dfx += ddfx; + dfy += ddfy; + ddfx += dddfx; + ddfy += dddfy; + x += dfx; + y += dfy; + } + } + + public float GetCurvePercent (int frameIndex, float percent) { + float[] curves = this.curves; + int i = frameIndex * BEZIER_SIZE; + float type = curves[i]; + if (type == LINEAR) return percent; + if (type == STEPPED) return 0; + i++; + float x = 0; + for (int start = i, n = i + BEZIER_SIZE - 1; i < n; i += 2) { + x = curves[i]; + if (x >= percent) { + float prevX, prevY; + if (i == start) { + prevX = 0; + prevY = 0; + } else { + prevX = curves[i - 2]; + prevY = curves[i - 1]; + } + return prevY + (curves[i + 1] - prevY) * (percent - prevX) / (x - prevX); + } + } + float y = curves[i - 1]; + return y + (1 - y) * (percent - x) / (1 - x); // Last point is 1,1. + } + public float GetCurveType (int frameIndex) { + return curves[frameIndex * BEZIER_SIZE]; + } + } + + public class RotateTimeline : CurveTimeline { + protected const int PREV_FRAME_TIME = -2; + protected const int FRAME_VALUE = 1; + + internal int boneIndex; + internal float[] frames; + + public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, value, ... + + public RotateTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount << 1]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, float angle) { + frameIndex *= 2; + frames[frameIndex] = time; + frames[frameIndex + 1] = angle; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + Bone bone = skeleton.bones[boneIndex]; + + float amount; + + if (time >= frames[frames.Length - 2]) { // Time is after last frame. + amount = bone.data.rotation + frames[frames.Length - 1] - bone.rotation; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + bone.rotation += amount * alpha; + return; + } + + // Interpolate between the previous frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 2); + float prevFrameValue = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime); + percent = GetCurvePercent((frameIndex >> 1) - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + amount = frames[frameIndex + FRAME_VALUE] - prevFrameValue; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + amount = bone.data.rotation + (prevFrameValue + amount * percent) - bone.rotation; + while (amount > 180) + amount -= 360; + while (amount < -180) + amount += 360; + bone.rotation += amount * alpha; + } + } + + public class TranslateTimeline : CurveTimeline { + protected const int PREV_FRAME_TIME = -3; + protected const int FRAME_X = 1; + protected const int FRAME_Y = 2; + + internal int boneIndex; + internal float[] frames; + + public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, value, value, ... + + public TranslateTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * 3]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, float x, float y) { + frameIndex *= 3; + frames[frameIndex] = time; + frames[frameIndex + 1] = x; + frames[frameIndex + 2] = y; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + Bone bone = skeleton.bones[boneIndex]; + + if (time >= frames[frames.Length - 3]) { // Time is after last frame. + bone.x += (bone.data.x + frames[frames.Length - 2] - bone.x) * alpha; + bone.y += (bone.data.y + frames[frames.Length - 1] - bone.y) * alpha; + return; + } + + // Interpolate between the previous frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 3); + float prevFrameX = frames[frameIndex - 2]; + float prevFrameY = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + bone.x += (bone.data.x + prevFrameX + (frames[frameIndex + FRAME_X] - prevFrameX) * percent - bone.x) * alpha; + bone.y += (bone.data.y + prevFrameY + (frames[frameIndex + FRAME_Y] - prevFrameY) * percent - bone.y) * alpha; + } + } + + public class ScaleTimeline : TranslateTimeline { + public ScaleTimeline (int frameCount) + : base(frameCount) { + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + Bone bone = skeleton.bones[boneIndex]; + if (time >= frames[frames.Length - 3]) { // Time is after last frame. + bone.scaleX += (bone.data.scaleX * frames[frames.Length - 2] - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY * frames[frames.Length - 1] - bone.scaleY) * alpha; + return; + } + + // Interpolate between the previous frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 3); + float prevFrameX = frames[frameIndex - 2]; + float prevFrameY = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + bone.scaleX += (bone.data.scaleX * (prevFrameX + (frames[frameIndex + FRAME_X] - prevFrameX) * percent) - bone.scaleX) * alpha; + bone.scaleY += (bone.data.scaleY * (prevFrameY + (frames[frameIndex + FRAME_Y] - prevFrameY) * percent) - bone.scaleY) * alpha; + } + } + + public class ColorTimeline : CurveTimeline { + protected const int PREV_FRAME_TIME = -5; + protected const int FRAME_R = 1; + protected const int FRAME_G = 2; + protected const int FRAME_B = 3; + protected const int FRAME_A = 4; + + internal int slotIndex; + internal float[] frames; + + public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, r, g, b, a, ... + + public ColorTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * 5]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, float r, float g, float b, float a) { + frameIndex *= 5; + frames[frameIndex] = time; + frames[frameIndex + 1] = r; + frames[frameIndex + 2] = g; + frames[frameIndex + 3] = b; + frames[frameIndex + 4] = a; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + float r, g, b, a; + if (time >= frames[frames.Length - 5]) { + // Time is after last frame. + int i = frames.Length - 1; + r = frames[i - 3]; + g = frames[i - 2]; + b = frames[i - 1]; + a = frames[i]; + } else { + // Interpolate between the previous frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 5); + float prevFrameR = frames[frameIndex - 4]; + float prevFrameG = frames[frameIndex - 3]; + float prevFrameB = frames[frameIndex - 2]; + float prevFrameA = frames[frameIndex - 1]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 5 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + r = prevFrameR + (frames[frameIndex + FRAME_R] - prevFrameR) * percent; + g = prevFrameG + (frames[frameIndex + FRAME_G] - prevFrameG) * percent; + b = prevFrameB + (frames[frameIndex + FRAME_B] - prevFrameB) * percent; + a = prevFrameA + (frames[frameIndex + FRAME_A] - prevFrameA) * percent; + } + Slot slot = skeleton.slots[slotIndex]; + if (alpha < 1) { + slot.r += (r - slot.r) * alpha; + slot.g += (g - slot.g) * alpha; + slot.b += (b - slot.b) * alpha; + slot.a += (a - slot.a) * alpha; + } else { + slot.r = r; + slot.g = g; + slot.b = b; + slot.a = a; + } + } + } + + public class AttachmentTimeline : Timeline { + internal int slotIndex; + internal float[] frames; + private String[] attachmentNames; + + public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public String[] AttachmentNames { get { return attachmentNames; } set { attachmentNames = value; } } + public int FrameCount { get { return frames.Length; } } + + public AttachmentTimeline (int frameCount) { + frames = new float[frameCount]; + attachmentNames = new String[frameCount]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, String attachmentName) { + frames[frameIndex] = time; + attachmentNames[frameIndex] = attachmentName; + } + + public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) { + if (lastTime > time) Apply(skeleton, lastTime, int.MaxValue, null, 0); + return; + } else if (lastTime > time) // + lastTime = -1; + + int frameIndex = (time >= frames[frames.Length - 1] ? frames.Length : Animation.binarySearch(frames, time)) - 1; + if (frames[frameIndex] < lastTime) return; + + String attachmentName = attachmentNames[frameIndex]; + skeleton.slots[slotIndex].Attachment = + attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); + } + } + + public class EventTimeline : Timeline { + internal float[] frames; + private Event[] events; + + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public Event[] Events { get { return events; } set { events = value; } } + public int FrameCount { get { return frames.Length; } } + + public EventTimeline (int frameCount) { + frames = new float[frameCount]; + events = new Event[frameCount]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, Event e) { + frames[frameIndex] = time; + events[frameIndex] = e; + } + + /// Fires events for frames > lastTime and <= time. + public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + if (firedEvents == null) return; + float[] frames = this.frames; + int frameCount = frames.Length; + + if (lastTime > time) { // Fire events after last time for looped animations. + Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha); + lastTime = -1f; + } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. + return; + if (time < frames[0]) return; // Time is before first frame. + + int frameIndex; + if (lastTime < frames[0]) + frameIndex = 0; + else { + frameIndex = Animation.binarySearch(frames, lastTime); + float frame = frames[frameIndex]; + while (frameIndex > 0) { // Fire multiple events with the same frame. + if (frames[frameIndex - 1] != frame) break; + frameIndex--; + } + } + for (; frameIndex < frameCount && time >= frames[frameIndex]; frameIndex++) + firedEvents.Add(events[frameIndex]); + } + } + + public class DrawOrderTimeline : Timeline { + internal float[] frames; + private int[][] drawOrders; + + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public int[][] DrawOrders { get { return drawOrders; } set { drawOrders = value; } } + public int FrameCount { get { return frames.Length; } } + + public DrawOrderTimeline (int frameCount) { + frames = new float[frameCount]; + drawOrders = new int[frameCount][]; + } + + /// Sets the time and value of the specified keyframe. + /// May be null to use bind pose draw order. + public void SetFrame (int frameIndex, float time, int[] drawOrder) { + frames[frameIndex] = time; + drawOrders[frameIndex] = drawOrder; + } + + public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + int frameIndex; + if (time >= frames[frames.Length - 1]) // Time is after last frame. + frameIndex = frames.Length - 1; + else + frameIndex = Animation.binarySearch(frames, time) - 1; + + List drawOrder = skeleton.drawOrder; + List slots = skeleton.slots; + int[] drawOrderToSetupIndex = drawOrders[frameIndex]; + if (drawOrderToSetupIndex == null) { + drawOrder.Clear(); + drawOrder.AddRange(slots); + } else { + for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++) + drawOrder[i] = slots[drawOrderToSetupIndex[i]]; + } + } + } + + public class FFDTimeline : CurveTimeline { + internal int slotIndex; + internal float[] frames; + private float[][] frameVertices; + internal Attachment attachment; + + public int SlotIndex { get { return slotIndex; } set { slotIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, ... + public float[][] Vertices { get { return frameVertices; } set { frameVertices = value; } } + public Attachment Attachment { get { return attachment; } set { attachment = value; } } + + public FFDTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount]; + frameVertices = new float[frameCount][]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, float[] vertices) { + frames[frameIndex] = time; + frameVertices[frameIndex] = vertices; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + Slot slot = skeleton.slots[slotIndex]; + if (slot.attachment != attachment) return; + + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + float[][] frameVertices = this.frameVertices; + int vertexCount = frameVertices[0].Length; + + float[] vertices = slot.attachmentVertices; + if (vertices.Length < vertexCount) { + vertices = new float[vertexCount]; + slot.attachmentVertices = vertices; + } + if (vertices.Length != vertexCount) alpha = 1; // Don't mix from uninitialized slot vertices. + slot.attachmentVerticesCount = vertexCount; + + if (time >= frames[frames.Length - 1]) { // Time is after last frame. + float[] lastVertices = frameVertices[frames.Length - 1]; + if (alpha < 1) { + for (int i = 0; i < vertexCount; i++) { + float vertex = vertices[i]; + vertices[i] = vertex + (lastVertices[i] - vertex) * alpha; + } + } else + Array.Copy(lastVertices, 0, vertices, 0, vertexCount); + return; + } + + // Interpolate between the previous frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time); + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex - 1] - frameTime); + percent = GetCurvePercent(frameIndex - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + float[] prevVertices = frameVertices[frameIndex - 1]; + float[] nextVertices = frameVertices[frameIndex]; + + if (alpha < 1) { + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + float vertex = vertices[i]; + vertices[i] = vertex + (prev + (nextVertices[i] - prev) * percent - vertex) * alpha; + } + } else { + for (int i = 0; i < vertexCount; i++) { + float prev = prevVertices[i]; + vertices[i] = prev + (nextVertices[i] - prev) * percent; + } + } + } + } + + public class IkConstraintTimeline : CurveTimeline { + private const int PREV_FRAME_TIME = -3; + private const int PREV_FRAME_MIX = -2; + private const int PREV_FRAME_BEND_DIRECTION = -1; + private const int FRAME_MIX = 1; + + internal int ikConstraintIndex; + internal float[] frames; + + public int IkConstraintIndex { get { return ikConstraintIndex; } set { ikConstraintIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, mix, bendDirection, ... + + public IkConstraintTimeline (int frameCount) + : base(frameCount) { + frames = new float[frameCount * 3]; + } + + /** Sets the time, mix and bend direction of the specified keyframe. */ + public void SetFrame (int frameIndex, float time, float mix, int bendDirection) { + frameIndex *= 3; + frames[frameIndex] = time; + frames[frameIndex + 1] = mix; + frames[frameIndex + 2] = bendDirection; + } + + override public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) return; // Time is before first frame. + + IkConstraint ikConstraint = skeleton.ikConstraints[ikConstraintIndex]; + + if (time >= frames[frames.Length - 3]) { // Time is after last frame. + ikConstraint.mix += (frames[frames.Length - 2] - ikConstraint.mix) * alpha; + ikConstraint.bendDirection = (int)frames[frames.Length - 1]; + return; + } + + // Interpolate between the previous frame and the current frame. + int frameIndex = Animation.binarySearch(frames, time, 3); + float prevFrameMix = frames[frameIndex + PREV_FRAME_MIX]; + float frameTime = frames[frameIndex]; + float percent = 1 - (time - frameTime) / (frames[frameIndex + PREV_FRAME_TIME] - frameTime); + percent = GetCurvePercent(frameIndex / 3 - 1, percent < 0 ? 0 : (percent > 1 ? 1 : percent)); + + float mix = prevFrameMix + (frames[frameIndex + FRAME_MIX] - prevFrameMix) * percent; + ikConstraint.mix += (mix - ikConstraint.mix) * alpha; + ikConstraint.bendDirection = (int)frames[frameIndex + PREV_FRAME_BEND_DIRECTION]; + } + } + + public class FlipXTimeline : Timeline { + internal int boneIndex; + internal float[] frames; + + public int BoneIndex { get { return boneIndex; } set { boneIndex = value; } } + public float[] Frames { get { return frames; } set { frames = value; } } // time, flip, ... + public int FrameCount { get { return frames.Length >> 1; } } + + public FlipXTimeline (int frameCount) { + frames = new float[frameCount << 1]; + } + + /// Sets the time and value of the specified keyframe. + public void SetFrame (int frameIndex, float time, bool flip) { + frameIndex *= 2; + frames[frameIndex] = time; + frames[frameIndex + 1] = flip ? 1 : 0; + } + + public void Apply (Skeleton skeleton, float lastTime, float time, List firedEvents, float alpha) { + float[] frames = this.frames; + if (time < frames[0]) { + if (lastTime > time) Apply(skeleton, lastTime, int.MaxValue, null, 0); + return; + } else if (lastTime > time) // + lastTime = -1; + + int frameIndex = (time >= frames[frames.Length - 2] ? frames.Length : Animation.binarySearch(frames, time, 2)) - 2; + if (frames[frameIndex] < lastTime) return; + + SetFlip(skeleton.bones[boneIndex], frames[frameIndex + 1] != 0); + } + + virtual protected void SetFlip (Bone bone, bool flip) { + bone.flipX = flip; + } + } + + public class FlipYTimeline : FlipXTimeline { + public FlipYTimeline (int frameCount) + : base(frameCount) { + } + + override protected void SetFlip (Bone bone, bool flip) { + bone.flipY = flip; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/AnimationState.cs b/SpineRuntimes/SpineRuntime21/AnimationState.cs new file mode 100644 index 0000000..c264c4a --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/AnimationState.cs @@ -0,0 +1,299 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.Text; + +namespace SpineRuntime21 { + public class AnimationState { + private AnimationStateData data; + private List tracks = new List(); + private List events = new List(); + private float timeScale = 1; + + public AnimationStateData Data { get { return data; } } + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + + public delegate void StartEndDelegate(AnimationState state, int trackIndex); + public event StartEndDelegate Start; + public event StartEndDelegate End; + + public delegate void EventDelegate(AnimationState state, int trackIndex, Event e); + public event EventDelegate Event; + + public delegate void CompleteDelegate(AnimationState state, int trackIndex, int loopCount); + public event CompleteDelegate Complete; + + public AnimationState (AnimationStateData data) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + this.data = data; + } + + public void Update (float delta) { + delta *= timeScale; + for (int i = 0; i < tracks.Count; i++) { + TrackEntry current = tracks[i]; + if (current == null) continue; + + float trackDelta = delta * current.timeScale; + float time = current.time + trackDelta; + float endTime = current.endTime; + + current.time = time; + if (current.previous != null) { + current.previous.time += trackDelta; + current.mixTime += trackDelta; + } + + // Check if completed the animation or a loop iteration. + if (current.loop ? (current.lastTime % endTime > time % endTime) : (current.lastTime < endTime && time >= endTime)) { + int count = (int)(time / endTime); + current.OnComplete(this, i, count); + if (Complete != null) Complete(this, i, count); + } + + TrackEntry next = current.next; + if (next != null) { + next.time = current.lastTime - next.delay; + if (next.time >= 0) SetCurrent(i, next); + } else { + // End non-looping animation when it reaches its end time and there is no next entry. + if (!current.loop && current.lastTime >= current.endTime) ClearTrack(i); + } + } + } + + public void Apply (Skeleton skeleton) { + List events = this.events; + + for (int i = 0; i < tracks.Count; i++) { + TrackEntry current = tracks[i]; + if (current == null) continue; + + events.Clear(); + + float time = current.time; + bool loop = current.loop; + if (!loop && time > current.endTime) time = current.endTime; + + TrackEntry previous = current.previous; + if (previous == null) { + if (current.mix == 1) + current.animation.Apply(skeleton, current.lastTime, time, loop, events); + else + current.animation.Mix(skeleton, current.lastTime, time, loop, events, current.mix); + } else { + float previousTime = previous.time; + if (!previous.loop && previousTime > previous.endTime) previousTime = previous.endTime; + previous.animation.Apply(skeleton, previousTime, previousTime, previous.loop, null); + + float alpha = current.mixTime / current.mixDuration * current.mix; + if (alpha >= 1) { + alpha = 1; + current.previous = null; + } + current.animation.Mix(skeleton, current.lastTime, time, loop, events, alpha); + } + + for (int ii = 0, nn = events.Count; ii < nn; ii++) { + Event e = events[ii]; + current.OnEvent(this, i, e); + if (Event != null) Event(this, i, e); + } + + current.lastTime = current.time; + } + } + + public void ClearTracks () { + for (int i = 0, n = tracks.Count; i < n; i++) + ClearTrack(i); + tracks.Clear(); + } + + public void ClearTrack (int trackIndex) { + if (trackIndex >= tracks.Count) return; + TrackEntry current = tracks[trackIndex]; + if (current == null) return; + + current.OnEnd(this, trackIndex); + if (End != null) End(this, trackIndex); + + tracks[trackIndex] = null; + } + + private TrackEntry ExpandToIndex (int index) { + if (index < tracks.Count) return tracks[index]; + while (index >= tracks.Count) + tracks.Add(null); + return null; + } + + private void SetCurrent (int index, TrackEntry entry) { + TrackEntry current = ExpandToIndex(index); + if (current != null) { + TrackEntry previous = current.previous; + current.previous = null; + + current.OnEnd(this, index); + if (End != null) End(this, index); + + entry.mixDuration = data.GetMix(current.animation, entry.animation); + if (entry.mixDuration > 0) { + entry.mixTime = 0; + // If a mix is in progress, mix from the closest animation. + if (previous != null && current.mixTime / current.mixDuration < 0.5f) + entry.previous = previous; + else + entry.previous = current; + } + } + + tracks[index] = entry; + + entry.OnStart(this, index); + if (Start != null) Start(this, index); + } + + public TrackEntry SetAnimation (int trackIndex, String animationName, bool loop) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName); + return SetAnimation(trackIndex, animation, loop); + } + + /// Set the current animation. Any queued animations are cleared. + public TrackEntry SetAnimation (int trackIndex, Animation animation, bool loop) { + if (animation == null) throw new ArgumentException("animation cannot be null."); + TrackEntry entry = new TrackEntry(); + entry.animation = animation; + entry.loop = loop; + entry.time = 0; + entry.endTime = animation.Duration; + SetCurrent(trackIndex, entry); + return entry; + } + + public TrackEntry AddAnimation (int trackIndex, String animationName, bool loop, float delay) { + Animation animation = data.skeletonData.FindAnimation(animationName); + if (animation == null) throw new ArgumentException("Animation not found: " + animationName); + return AddAnimation(trackIndex, animation, loop, delay); + } + + /// Adds an animation to be played delay seconds after the current or last queued animation. + /// May be <= 0 to use duration of previous animation minus any mix duration plus the negative delay. + public TrackEntry AddAnimation (int trackIndex, Animation animation, bool loop, float delay) { + if (animation == null) throw new ArgumentException("animation cannot be null."); + TrackEntry entry = new TrackEntry(); + entry.animation = animation; + entry.loop = loop; + entry.time = 0; + entry.endTime = animation.Duration; + + TrackEntry last = ExpandToIndex(trackIndex); + if (last != null) { + while (last.next != null) + last = last.next; + last.next = entry; + } else + tracks[trackIndex] = entry; + + if (delay <= 0) { + if (last != null) + delay += last.endTime - data.GetMix(last.animation, animation); + else + delay = 0; + } + entry.delay = delay; + + return entry; + } + + /// May be null. + public TrackEntry GetCurrent (int trackIndex) { + if (trackIndex >= tracks.Count) return null; + return tracks[trackIndex]; + } + + override public String ToString () { + StringBuilder buffer = new StringBuilder(); + for (int i = 0, n = tracks.Count; i < n; i++) { + TrackEntry entry = tracks[i]; + if (entry == null) continue; + if (buffer.Length > 0) buffer.Append(", "); + buffer.Append(entry.ToString()); + } + if (buffer.Length == 0) return ""; + return buffer.ToString(); + } + } + + public class TrackEntry { + internal TrackEntry next, previous; + internal Animation animation; + internal bool loop; + internal float delay, time, lastTime = -1, endTime, timeScale = 1; + internal float mixTime, mixDuration, mix = 1; + + public Animation Animation { get { return animation; } } + public float Delay { get { return delay; } set { delay = value; } } + public float Time { get { return time; } set { time = value; } } + public float LastTime { get { return lastTime; } set { lastTime = value; } } + public float EndTime { get { return endTime; } set { endTime = value; } } + public float TimeScale { get { return timeScale; } set { timeScale = value; } } + public float Mix { get { return mix; } set { mix = value; } } + public bool Loop { get { return loop; } set { loop = value; } } + + public event AnimationState.StartEndDelegate Start; + public event AnimationState.StartEndDelegate End; + public event AnimationState.EventDelegate Event; + public event AnimationState.CompleteDelegate Complete; + + internal void OnStart (AnimationState state, int index) { + if (Start != null) Start(state, index); + } + + internal void OnEnd (AnimationState state, int index) { + if (End != null) End(state, index); + } + + internal void OnEvent (AnimationState state, int index, Event e) { + if (Event != null) Event(state, index, e); + } + + internal void OnComplete (AnimationState state, int index, int loopCount) { + if (Complete != null) Complete(state, index, loopCount); + } + + override public String ToString () { + return animation == null ? "" : animation.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/AnimationStateData.cs b/SpineRuntimes/SpineRuntime21/AnimationStateData.cs new file mode 100644 index 0000000..56499c6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/AnimationStateData.cs @@ -0,0 +1,70 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class AnimationStateData { + internal SkeletonData skeletonData; + private Dictionary, float> animationToMixTime = new Dictionary, float>(); + internal float defaultMix; + + public SkeletonData SkeletonData { get { return skeletonData; } } + public float DefaultMix { get { return defaultMix; } set { defaultMix = value; } } + + public AnimationStateData (SkeletonData skeletonData) { + this.skeletonData = skeletonData; + } + + public void SetMix (String fromName, String toName, float duration) { + Animation from = skeletonData.FindAnimation(fromName); + if (from == null) throw new ArgumentException("Animation not found: " + fromName); + Animation to = skeletonData.FindAnimation(toName); + if (to == null) throw new ArgumentException("Animation not found: " + toName); + SetMix(from, to, duration); + } + + public void SetMix (Animation from, Animation to, float duration) { + if (from == null) throw new ArgumentNullException("from cannot be null."); + if (to == null) throw new ArgumentNullException("to cannot be null."); + KeyValuePair key = new KeyValuePair(from, to); + animationToMixTime.Remove(key); + animationToMixTime.Add(key, duration); + } + + public float GetMix (Animation from, Animation to) { + KeyValuePair key = new KeyValuePair(from, to); + float duration; + if (animationToMixTime.TryGetValue(key, out duration)) return duration; + return defaultMix; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Atlas.cs b/SpineRuntimes/SpineRuntime21/Atlas.cs new file mode 100644 index 0000000..8f0d205 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Atlas.cs @@ -0,0 +1,288 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace SpineRuntime21 { + public class Atlas { + List pages = new List(); + List regions = new List(); + TextureLoader textureLoader; + +#if WINDOWS_STOREAPP + private async Task ReadFile(string path, TextureLoader textureLoader) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + try { + Load(reader, Path.GetDirectoryName(path), textureLoader); + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } + + public Atlas(String path, TextureLoader textureLoader) { + this.ReadFile(path, textureLoader).Wait(); + } +#else + public Atlas (String path, TextureLoader textureLoader) { + +#if WINDOWS_PHONE + Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path); + using (StreamReader reader = new StreamReader(stream)) + { +#else + using (StreamReader reader = new StreamReader(path)) { +#endif + try { + Load(reader, Path.GetDirectoryName(path), textureLoader); + } catch (Exception ex) { + throw new Exception("Error reading atlas file: " + path, ex); + } + } + } +#endif + + public Atlas (TextReader reader, String dir, TextureLoader textureLoader) { + Load(reader, dir, textureLoader); + } + + public Atlas (List pages, List regions) { + this.pages = pages; + this.regions = regions; + this.textureLoader = null; + } + + private void Load (TextReader reader, String imagesDir, TextureLoader textureLoader) { + if (textureLoader == null) throw new ArgumentNullException("textureLoader cannot be null."); + this.textureLoader = textureLoader; + + String[] tuple = new String[4]; + AtlasPage page = null; + while (true) { + String line = reader.ReadLine(); + if (line == null) break; + if (line.Trim().Length == 0) + page = null; + else if (page == null) { + page = new AtlasPage(); + page.name = line; + + if (readTuple(reader, tuple) == 2) { // size is only optional for an atlas packed with an old TexturePacker. + page.width = int.Parse(tuple[0]); + page.height = int.Parse(tuple[1]); + readTuple(reader, tuple); + } + page.format = (Format)Enum.Parse(typeof(Format), tuple[0], false); + + readTuple(reader, tuple); + page.minFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[0], false); + page.magFilter = (TextureFilter)Enum.Parse(typeof(TextureFilter), tuple[1], false); + + String direction = readValue(reader); + page.uWrap = TextureWrap.ClampToEdge; + page.vWrap = TextureWrap.ClampToEdge; + if (direction == "x") + page.uWrap = TextureWrap.Repeat; + else if (direction == "y") + page.vWrap = TextureWrap.Repeat; + else if (direction == "xy") + page.uWrap = page.vWrap = TextureWrap.Repeat; + + textureLoader.Load(page, Path.Combine(imagesDir, line)); + + pages.Add(page); + + } else { + AtlasRegion region = new AtlasRegion(); + region.name = line; + region.page = page; + + region.rotate = Boolean.Parse(readValue(reader)); + + readTuple(reader, tuple); + int x = int.Parse(tuple[0]); + int y = int.Parse(tuple[1]); + + readTuple(reader, tuple); + int width = int.Parse(tuple[0]); + int height = int.Parse(tuple[1]); + + region.u = x / (float)page.width; + region.v = y / (float)page.height; + if (region.rotate) { + region.u2 = (x + height) / (float)page.width; + region.v2 = (y + width) / (float)page.height; + } else { + region.u2 = (x + width) / (float)page.width; + region.v2 = (y + height) / (float)page.height; + } + region.x = x; + region.y = y; + region.width = Math.Abs(width); + region.height = Math.Abs(height); + + if (readTuple(reader, tuple) == 4) { // split is optional + region.splits = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]), + int.Parse(tuple[2]), int.Parse(tuple[3])}; + + if (readTuple(reader, tuple) == 4) { // pad is optional, but only present with splits + region.pads = new int[] {int.Parse(tuple[0]), int.Parse(tuple[1]), + int.Parse(tuple[2]), int.Parse(tuple[3])}; + + readTuple(reader, tuple); + } + } + + region.originalWidth = int.Parse(tuple[0]); + region.originalHeight = int.Parse(tuple[1]); + + readTuple(reader, tuple); + region.offsetX = int.Parse(tuple[0]); + region.offsetY = int.Parse(tuple[1]); + + region.index = int.Parse(readValue(reader)); + + regions.Add(region); + } + } + } + + static String readValue (TextReader reader) { + String line = reader.ReadLine(); + int colon = line.IndexOf(':'); + if (colon == -1) throw new Exception("Invalid line: " + line); + return line.Substring(colon + 1).Trim(); + } + + /// Returns the number of tuple values read (1, 2 or 4). + static int readTuple (TextReader reader, String[] tuple) { + String line = reader.ReadLine(); + int colon = line.IndexOf(':'); + if (colon == -1) throw new Exception("Invalid line: " + line); + int i = 0, lastMatch = colon + 1; + for (; i < 3; i++) { + int comma = line.IndexOf(',', lastMatch); + if (comma == -1) break; + tuple[i] = line.Substring(lastMatch, comma - lastMatch).Trim(); + lastMatch = comma + 1; + } + tuple[i] = line.Substring(lastMatch).Trim(); + return i + 1; + } + + public void FlipV () { + for (int i = 0, n = regions.Count; i < n; i++) { + AtlasRegion region = regions[i]; + region.v = 1 - region.v; + region.v2 = 1 - region.v2; + } + } + + /// Returns the first region found with the specified name. This method uses string comparison to find the region, so the result + /// should be cached rather than calling this method multiple times. + /// The region, or null. + public AtlasRegion FindRegion (String name) { + for (int i = 0, n = regions.Count; i < n; i++) + if (regions[i].name == name) return regions[i]; + return null; + } + + public void Dispose () { + if (textureLoader == null) return; + for (int i = 0, n = pages.Count; i < n; i++) + textureLoader.Unload(pages[i].rendererObject); + } + } + + public enum Format { + Alpha, + Intensity, + LuminanceAlpha, + RGB565, + RGBA4444, + RGB888, + RGBA8888 + } + + public enum TextureFilter { + Nearest, + Linear, + MipMap, + MipMapNearestNearest, + MipMapLinearNearest, + MipMapNearestLinear, + MipMapLinearLinear + } + + public enum TextureWrap { + MirroredRepeat, + ClampToEdge, + Repeat + } + + public class AtlasPage { + public String name; + public Format format; + public TextureFilter minFilter; + public TextureFilter magFilter; + public TextureWrap uWrap; + public TextureWrap vWrap; + public Object rendererObject; + public int width, height; + } + + public class AtlasRegion { + public AtlasPage page; + public String name; + public int x, y, width, height; + public float u, v, u2, v2; + public float offsetX, offsetY; + public int originalWidth, originalHeight; + public int index; + public bool rotate; + public int[] splits; + public int[] pads; + } + + public interface TextureLoader { + void Load (AtlasPage page, String path); + void Unload (Object texture); + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/AtlasAttachmentLoader.cs b/SpineRuntimes/SpineRuntime21/Attachments/AtlasAttachmentLoader.cs new file mode 100644 index 0000000..2a9ca87 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/AtlasAttachmentLoader.cs @@ -0,0 +1,111 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public class AtlasAttachmentLoader : AttachmentLoader { + private Atlas[] atlasArray; + + public AtlasAttachmentLoader (params Atlas[] atlasArray) { + if (atlasArray == null) throw new ArgumentNullException("atlas array cannot be null."); + this.atlasArray = atlasArray; + } + + public RegionAttachment NewRegionAttachment (Skin skin, String name, String path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new Exception("Region not found in atlas: " + path + " (region attachment: " + name + ")"); + RegionAttachment attachment = new RegionAttachment(name); + attachment.RendererObject = region; + attachment.SetUVs(region.u, region.v, region.u2, region.v2, region.rotate); + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public MeshAttachment NewMeshAttachment (Skin skin, String name, String path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new Exception("Region not found in atlas: " + path + " (mesh attachment: " + name + ")"); + MeshAttachment attachment = new MeshAttachment(name); + attachment.RendererObject = region; + attachment.RegionU = region.u; + attachment.RegionV = region.v; + attachment.RegionU2 = region.u2; + attachment.RegionV2 = region.v2; + attachment.RegionRotate = region.rotate; + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public SkinnedMeshAttachment NewSkinnedMeshAttachment (Skin skin, String name, String path) { + AtlasRegion region = FindRegion(path); + if (region == null) throw new Exception("Region not found in atlas: " + path + " (skinned mesh attachment: " + name + ")"); + SkinnedMeshAttachment attachment = new SkinnedMeshAttachment(name); + attachment.RendererObject = region; + attachment.RegionU = region.u; + attachment.RegionV = region.v; + attachment.RegionU2 = region.u2; + attachment.RegionV2 = region.v2; + attachment.RegionRotate = region.rotate; + attachment.regionOffsetX = region.offsetX; + attachment.regionOffsetY = region.offsetY; + attachment.regionWidth = region.width; + attachment.regionHeight = region.height; + attachment.regionOriginalWidth = region.originalWidth; + attachment.regionOriginalHeight = region.originalHeight; + return attachment; + } + + public BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, String name) { + return new BoundingBoxAttachment(name); + } + + public AtlasRegion FindRegion(string name) { + AtlasRegion region; + + for (int i = 0; i < atlasArray.Length; i++) { + region = atlasArray[i].FindRegion(name); + if (region != null) + return region; + } + + return null; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/Attachment.cs b/SpineRuntimes/SpineRuntime21/Attachments/Attachment.cs new file mode 100644 index 0000000..b6a4e2f --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/Attachment.cs @@ -0,0 +1,46 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + abstract public class Attachment { + public String Name { get; private set; } + + public Attachment (String name) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + Name = name; + } + + override public String ToString () { + return Name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/AttachmentLoader.cs b/SpineRuntimes/SpineRuntime21/Attachments/AttachmentLoader.cs new file mode 100644 index 0000000..824113e --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/AttachmentLoader.cs @@ -0,0 +1,47 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public interface AttachmentLoader { + /// May be null to not load any attachment. + RegionAttachment NewRegionAttachment (Skin skin, String name, String path); + + /// May be null to not load any attachment. + MeshAttachment NewMeshAttachment (Skin skin, String name, String path); + + /// May be null to not load any attachment. + SkinnedMeshAttachment NewSkinnedMeshAttachment (Skin skin, String name, String path); + + /// May be null to not load any attachment. + BoundingBoxAttachment NewBoundingBoxAttachment (Skin skin, String name); + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/AttachmentType.cs b/SpineRuntimes/SpineRuntime21/Attachments/AttachmentType.cs new file mode 100644 index 0000000..c9e3926 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/AttachmentType.cs @@ -0,0 +1,35 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +namespace SpineRuntime21 { + public enum AttachmentType { + region, boundingbox, mesh, skinnedmesh + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/BoundingBoxAttachment.cs b/SpineRuntimes/SpineRuntime21/Attachments/BoundingBoxAttachment.cs new file mode 100644 index 0000000..153ff9f --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/BoundingBoxAttachment.cs @@ -0,0 +1,60 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + /// Attachment that has a polygon for bounds checking. + public class BoundingBoxAttachment : Attachment { + internal float[] vertices; + + public float[] Vertices { get { return vertices; } set { vertices = value; } } + + public BoundingBoxAttachment (string name) + : base(name) { + } + + /// Must have at least the same length as this attachment's vertices. + public void ComputeWorldVertices (Bone bone, float[] worldVertices) { + float x = bone.skeleton.x + bone.worldX, y = bone.skeleton.y + bone.worldY; + float m00 = bone.m00; + float m01 = bone.m01; + float m10 = bone.m10; + float m11 = bone.m11; + float[] vertices = this.vertices; + for (int i = 0, n = vertices.Length; i < n; i += 2) { + float px = vertices[i]; + float py = vertices[i + 1]; + worldVertices[i] = px * m00 + py * m01 + x; + worldVertices[i + 1] = px * m10 + py * m11 + y; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/MeshAttachment.cs b/SpineRuntimes/SpineRuntime21/Attachments/MeshAttachment.cs new file mode 100644 index 0000000..6617fd9 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/MeshAttachment.cs @@ -0,0 +1,108 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + /// Attachment that displays a texture region. + public class MeshAttachment : Attachment { + internal float[] vertices, uvs, regionUVs; + internal int[] triangles; + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + internal float r = 1, g = 1, b = 1, a = 1; + + public int HullLength { get; set; } + public float[] Vertices { get { return vertices; } set { vertices = value; } } + public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } } + public float[] UVs { get { return uvs; } set { uvs = value; } } + public int[] Triangles { get { return triangles; } set { triangles = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public String Path { get; set; } + public Object RendererObject { get; set; } + public float RegionU { get; set; } + public float RegionV { get; set; } + public float RegionU2 { get; set; } + public float RegionV2 { get; set; } + public bool RegionRotate { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + // Nonessential. + public int[] Edges { get; set; } + public float Width { get; set; } + public float Height { get; set; } + + public MeshAttachment (string name) + : base(name) { + } + + public void UpdateUVs () { + float u = RegionU, v = RegionV, width = RegionU2 - RegionU, height = RegionV2 - RegionV; + float[] regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length]; + float[] uvs = this.uvs; + if (RegionRotate) { + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + height - regionUVs[i] * height; + } + } else { + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + } + + public void ComputeWorldVertices (Slot slot, float[] worldVertices) { + Bone bone = slot.bone; + float x = bone.skeleton.x + bone.worldX, y = bone.skeleton.y + bone.worldY; + float m00 = bone.m00, m01 = bone.m01, m10 = bone.m10, m11 = bone.m11; + float[] vertices = this.vertices; + int verticesCount = vertices.Length; + if (slot.attachmentVerticesCount == verticesCount) vertices = slot.AttachmentVertices; + for (int i = 0; i < verticesCount; i += 2) { + float vx = vertices[i]; + float vy = vertices[i + 1]; + worldVertices[i] = vx * m00 + vy * m01 + x; + worldVertices[i + 1] = vx * m10 + vy * m11 + y; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/RegionAttachment.cs b/SpineRuntimes/SpineRuntime21/Attachments/RegionAttachment.cs new file mode 100644 index 0000000..e0e37be --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/RegionAttachment.cs @@ -0,0 +1,151 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + /// Attachment that displays a texture region. + public class RegionAttachment : Attachment { + public const int X1 = 0; + public const int Y1 = 1; + public const int X2 = 2; + public const int Y2 = 3; + public const int X3 = 4; + public const int Y3 = 5; + public const int X4 = 6; + public const int Y4 = 7; + + internal float x, y, rotation, scaleX = 1, scaleY = 1, width, height; + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + internal float[] offset = new float[8], uvs = new float[8]; + internal float r = 1, g = 1, b = 1, a = 1; + + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public String Path { get; set; } + public Object RendererObject { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + public float[] Offset { get { return offset; } } + public float[] UVs { get { return uvs; } } + + public RegionAttachment (string name) + : base(name) { + } + + public void SetUVs (float u, float v, float u2, float v2, bool rotate) { + float[] uvs = this.uvs; + if (rotate) { + uvs[X2] = u; + uvs[Y2] = v2; + uvs[X3] = u; + uvs[Y3] = v; + uvs[X4] = u2; + uvs[Y4] = v; + uvs[X1] = u2; + uvs[Y1] = v2; + } else { + uvs[X1] = u; + uvs[Y1] = v2; + uvs[X2] = u; + uvs[Y2] = v; + uvs[X3] = u2; + uvs[Y3] = v; + uvs[X4] = u2; + uvs[Y4] = v2; + } + } + + public void UpdateOffset () { + float width = this.width; + float height = this.height; + float scaleX = this.scaleX; + float scaleY = this.scaleY; + float regionScaleX = width / regionOriginalWidth * scaleX; + float regionScaleY = height / regionOriginalHeight * scaleY; + float localX = -width / 2 * scaleX + regionOffsetX * regionScaleX; + float localY = -height / 2 * scaleY + regionOffsetY * regionScaleY; + float localX2 = localX + regionWidth * regionScaleX; + float localY2 = localY + regionHeight * regionScaleY; + float radians = rotation * (float)Math.PI / 180; + float cos = (float)Math.Cos(radians); + float sin = (float)Math.Sin(radians); + float x = this.x; + float y = this.y; + float localXCos = localX * cos + x; + float localXSin = localX * sin; + float localYCos = localY * cos + y; + float localYSin = localY * sin; + float localX2Cos = localX2 * cos + x; + float localX2Sin = localX2 * sin; + float localY2Cos = localY2 * cos + y; + float localY2Sin = localY2 * sin; + float[] offset = this.offset; + offset[X1] = localXCos - localYSin; + offset[Y1] = localYCos + localXSin; + offset[X2] = localXCos - localY2Sin; + offset[Y2] = localY2Cos + localXSin; + offset[X3] = localX2Cos - localY2Sin; + offset[Y3] = localY2Cos + localX2Sin; + offset[X4] = localX2Cos - localYSin; + offset[Y4] = localYCos + localX2Sin; + } + + public void ComputeWorldVertices (Bone bone, float[] worldVertices) { + float x = bone.skeleton.x + bone.worldX, y = bone.skeleton.y + bone.worldY; + float m00 = bone.m00, m01 = bone.m01, m10 = bone.m10, m11 = bone.m11; + float[] offset = this.offset; + worldVertices[X1] = offset[X1] * m00 + offset[Y1] * m01 + x; + worldVertices[Y1] = offset[X1] * m10 + offset[Y1] * m11 + y; + worldVertices[X2] = offset[X2] * m00 + offset[Y2] * m01 + x; + worldVertices[Y2] = offset[X2] * m10 + offset[Y2] * m11 + y; + worldVertices[X3] = offset[X3] * m00 + offset[Y3] * m01 + x; + worldVertices[Y3] = offset[X3] * m10 + offset[Y3] * m11 + y; + worldVertices[X4] = offset[X4] * m00 + offset[Y4] * m01 + x; + worldVertices[Y4] = offset[X4] * m10 + offset[Y4] * m11 + y; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Attachments/SkinnedMeshAttachment.cs b/SpineRuntimes/SpineRuntime21/Attachments/SkinnedMeshAttachment.cs new file mode 100644 index 0000000..0b623e9 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Attachments/SkinnedMeshAttachment.cs @@ -0,0 +1,132 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + /// Attachment that displays a texture region. + public class SkinnedMeshAttachment : Attachment { + internal int[] bones; + internal float[] weights, uvs, regionUVs; + internal int[] triangles; + internal float regionOffsetX, regionOffsetY, regionWidth, regionHeight, regionOriginalWidth, regionOriginalHeight; + internal float r = 1, g = 1, b = 1, a = 1; + + public int HullLength { get; set; } + public int[] Bones { get { return bones; } set { bones = value; } } + public float[] Weights { get { return weights; } set { weights = value; } } + public float[] RegionUVs { get { return regionUVs; } set { regionUVs = value; } } + public float[] UVs { get { return uvs; } set { uvs = value; } } + public int[] Triangles { get { return triangles; } set { triangles = value; } } + + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + public String Path { get; set; } + public Object RendererObject { get; set; } + public float RegionU { get; set; } + public float RegionV { get; set; } + public float RegionU2 { get; set; } + public float RegionV2 { get; set; } + public bool RegionRotate { get; set; } + public float RegionOffsetX { get { return regionOffsetX; } set { regionOffsetX = value; } } + public float RegionOffsetY { get { return regionOffsetY; } set { regionOffsetY = value; } } // Pixels stripped from the bottom left, unrotated. + public float RegionWidth { get { return regionWidth; } set { regionWidth = value; } } + public float RegionHeight { get { return regionHeight; } set { regionHeight = value; } } // Unrotated, stripped size. + public float RegionOriginalWidth { get { return regionOriginalWidth; } set { regionOriginalWidth = value; } } + public float RegionOriginalHeight { get { return regionOriginalHeight; } set { regionOriginalHeight = value; } } // Unrotated, unstripped size. + + // Nonessential. + public int[] Edges { get; set; } + public float Width { get; set; } + public float Height { get; set; } + + public SkinnedMeshAttachment (string name) + : base(name) { + } + + public void UpdateUVs () { + float u = RegionU, v = RegionV, width = RegionU2 - RegionU, height = RegionV2 - RegionV; + float[] regionUVs = this.regionUVs; + if (this.uvs == null || this.uvs.Length != regionUVs.Length) this.uvs = new float[regionUVs.Length]; + float[] uvs = this.uvs; + if (RegionRotate) { + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i + 1] * width; + uvs[i + 1] = v + height - regionUVs[i] * height; + } + } else { + for (int i = 0, n = uvs.Length; i < n; i += 2) { + uvs[i] = u + regionUVs[i] * width; + uvs[i + 1] = v + regionUVs[i + 1] * height; + } + } + } + + public void ComputeWorldVertices (Slot slot, float[] worldVertices) { + Skeleton skeleton = slot.bone.skeleton; + List skeletonBones = skeleton.bones; + float x = skeleton.x, y = skeleton.y; + float[] weights = this.weights; + int[] bones = this.bones; + if (slot.attachmentVerticesCount == 0) { + for (int w = 0, v = 0, b = 0, n = bones.Length; v < n; w += 2) { + float wx = 0, wy = 0; + int nn = bones[v++] + v; + for (; v < nn; v++, b += 3) { + Bone bone = skeletonBones[bones[v]]; + float vx = weights[b], vy = weights[b + 1], weight = weights[b + 2]; + wx += (vx * bone.m00 + vy * bone.m01 + bone.worldX) * weight; + wy += (vx * bone.m10 + vy * bone.m11 + bone.worldY) * weight; + } + worldVertices[w] = wx + x; + worldVertices[w + 1] = wy + y; + } + } else { + float[] ffd = slot.AttachmentVertices; + for (int w = 0, v = 0, b = 0, f = 0, n = bones.Length; v < n; w += 2) { + float wx = 0, wy = 0; + int nn = bones[v++] + v; + for (; v < nn; v++, b += 3, f += 2) { + Bone bone = skeletonBones[bones[v]]; + float vx = weights[b] + ffd[f], vy = weights[b + 1] + ffd[f + 1], weight = weights[b + 2]; + wx += (vx * bone.m00 + vy * bone.m01 + bone.worldX) * weight; + wy += (vx * bone.m10 + vy * bone.m11 + bone.worldY) * weight; + } + worldVertices[w] = wx + x; + worldVertices[w + 1] = wy + y; + } + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Bone.cs b/SpineRuntimes/SpineRuntime21/Bone.cs new file mode 100644 index 0000000..286fd6a --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Bone.cs @@ -0,0 +1,165 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class Bone{ + static public bool yDown; + + internal BoneData data; + internal Skeleton skeleton; + internal Bone parent; + internal List children = new List(); + internal float x, y, rotation, rotationIK, scaleX, scaleY; + internal bool flipX, flipY; + internal float m00, m01, m10, m11; + internal float worldX, worldY, worldRotation, worldScaleX, worldScaleY; + internal bool worldFlipX, worldFlipY; + + public BoneData Data { get { return data; } } + public Skeleton Skeleton { get { return skeleton; } } + public Bone Parent { get { return parent; } } + public List Children { get { return children; } } + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + /// The forward kinetics rotation. + public float Rotation { get { return rotation; } set { rotation = value; } } + /// The inverse kinetics rotation, as calculated by any IK constraints. + public float RotationIK { get { return rotationIK; } set { rotationIK = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + public bool FlipX { get { return flipX; } set { flipX = value; } } + public bool FlipY { get { return flipY; } set { flipY = value; } } + + public float M00 { get { return m00; } } + public float M01 { get { return m01; } } + public float M10 { get { return m10; } } + public float M11 { get { return m11; } } + public float WorldX { get { return worldX; } } + public float WorldY { get { return worldY; } } + public float WorldRotation { get { return worldRotation; } } + public float WorldScaleX { get { return worldScaleX; } } + public float WorldScaleY { get { return worldScaleY; } } + public bool WorldFlipX { get { return worldFlipX; } set { worldFlipX = value; } } + public bool WorldFlipY { get { return worldFlipY; } set { worldFlipY = value; } } + + /// May be null. + public Bone (BoneData data, Skeleton skeleton, Bone parent) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + this.data = data; + this.skeleton = skeleton; + this.parent = parent; + SetToSetupPose(); + } + + /// Computes the world SRT using the parent bone and the local SRT. + public void UpdateWorldTransform () { + Bone parent = this.parent; + float x = this.x, y = this.y; + if (parent != null) { + worldX = x * parent.m00 + y * parent.m01 + parent.worldX; + worldY = x * parent.m10 + y * parent.m11 + parent.worldY; + if (data.inheritScale) { + worldScaleX = parent.worldScaleX * scaleX; + worldScaleY = parent.worldScaleY * scaleY; + } else { + worldScaleX = scaleX; + worldScaleY = scaleY; + } + worldRotation = data.inheritRotation ? parent.worldRotation + rotationIK : rotationIK; + worldFlipX = parent.worldFlipX != flipX; + worldFlipY = parent.worldFlipY != flipY; + } else { + Skeleton skeleton = this.skeleton; + bool skeletonFlipX = skeleton.flipX, skeletonFlipY = skeleton.flipY; + worldX = skeletonFlipX ? -x : x; + worldY = skeletonFlipY != yDown ? -y : y; + worldScaleX = scaleX; + worldScaleY = scaleY; + worldRotation = rotationIK; + worldFlipX = skeletonFlipX != flipX; + worldFlipY = skeletonFlipY != flipY; + } + float radians = worldRotation * (float)Math.PI / 180; + float cos = (float)Math.Cos(radians); + float sin = (float)Math.Sin(radians); + if (worldFlipX) { + m00 = -cos * worldScaleX; + m01 = sin * worldScaleY; + } else { + m00 = cos * worldScaleX; + m01 = -sin * worldScaleY; + } + if (worldFlipY != yDown) { + m10 = -sin * worldScaleX; + m11 = -cos * worldScaleY; + } else { + m10 = sin * worldScaleX; + m11 = cos * worldScaleY; + } + } + + public void SetToSetupPose () { + BoneData data = this.data; + x = data.x; + y = data.y; + rotation = data.rotation; + rotationIK = rotation; + scaleX = data.scaleX; + scaleY = data.scaleY; + flipX = data.flipX; + flipY = data.flipY; + } + + public void worldToLocal (float worldX, float worldY, out float localX, out float localY) { + float dx = worldX - this.worldX, dy = worldY - this.worldY; + float m00 = this.m00, m10 = this.m10, m01 = this.m01, m11 = this.m11; + if (worldFlipX != (worldFlipY != yDown)) { + m00 = -m00; + m11 = -m11; + } + float invDet = 1 / (m00 * m11 - m01 * m10); + localX = (dx * m00 * invDet - dy * m01 * invDet); + localY = (dy * m11 * invDet - dx * m10 * invDet); + } + + public void localToWorld (float localX, float localY, out float worldX, out float worldY) { + worldX = localX * m00 + localY * m01 + this.worldX; + worldY = localX * m10 + localY * m11 + this.worldY; + } + + override public String ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/BoneData.cs b/SpineRuntimes/SpineRuntime21/BoneData.cs new file mode 100644 index 0000000..237522c --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/BoneData.cs @@ -0,0 +1,66 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public class BoneData { + internal BoneData parent; + internal String name; + internal float length, x, y, rotation, scaleX = 1, scaleY = 1; + internal bool flipX, flipY; + internal bool inheritScale = true, inheritRotation = true; + + /// May be null. + public BoneData Parent { get { return parent; } } + public String Name { get { return name; } } + public float Length { get { return length; } set { length = value; } } + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public float Rotation { get { return rotation; } set { rotation = value; } } + public float ScaleX { get { return scaleX; } set { scaleX = value; } } + public float ScaleY { get { return scaleY; } set { scaleY = value; } } + public bool FlipX { get { return flipX; } set { flipX = value; } } + public bool FlipY { get { return flipY; } set { flipY = value; } } + public bool InheritScale { get { return inheritScale; } set { inheritScale = value; } } + public bool InheritRotation { get { return inheritRotation; } set { inheritRotation = value; } } + + /// May be null. + public BoneData (String name, BoneData parent) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + this.name = name; + this.parent = parent; + } + + override public String ToString () { + return name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Event.cs b/SpineRuntimes/SpineRuntime21/Event.cs new file mode 100644 index 0000000..84fad76 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Event.cs @@ -0,0 +1,48 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public class Event { + public EventData Data { get; private set; } + public int Int { get; set; } + public float Float { get; set; } + public String String { get; set; } + + public Event (EventData data) { + Data = data; + } + + override public String ToString () { + return Data.Name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/EventData.cs b/SpineRuntimes/SpineRuntime21/EventData.cs new file mode 100644 index 0000000..d1b70dd --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/EventData.cs @@ -0,0 +1,51 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public class EventData { + internal String name; + + public String Name { get { return name; } } + public int Int { get; set; } + public float Float { get; set; } + public String String { get; set; } + + public EventData (String name) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + this.name = name; + } + + override public String ToString () { + return Name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/IkConstraint.cs b/SpineRuntimes/SpineRuntime21/IkConstraint.cs new file mode 100644 index 0000000..4777a73 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/IkConstraint.cs @@ -0,0 +1,150 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class IkConstraint { + private const float radDeg = 180 / (float)Math.PI; + + internal IkConstraintData data; + internal List bones = new List(); + internal Bone target; + internal int bendDirection; + internal float mix; + + public IkConstraintData Data { get { return data; } } + public List Bones { get { return bones; } } + public Bone Target { get { return target; } set { target = value; } } + public int BendDirection { get { return bendDirection; } set { bendDirection = value; } } + public float Mix { get { return mix; } set { mix = value; } } + + public IkConstraint (IkConstraintData data, Skeleton skeleton) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + if (skeleton == null) throw new ArgumentNullException("skeleton cannot be null."); + this.data = data; + mix = data.mix; + bendDirection = data.bendDirection; + + bones = new List(data.bones.Count); + foreach (BoneData boneData in data.bones) + bones.Add(skeleton.FindBone(boneData.name)); + target = skeleton.FindBone(data.target.name); + } + + public void apply () { + Bone target = this.target; + List bones = this.bones; + switch (bones.Count) { + case 1: + apply(bones[0], target.worldX, target.worldY, mix); + break; + case 2: + apply(bones[0], bones[1], target.worldX, target.worldY, bendDirection, mix); + break; + } + } + + override public String ToString () { + return data.name; + } + + /// Adjusts the bone rotation so the tip is as close to the target position as possible. The target is specified + /// in the world coordinate system. + static public void apply (Bone bone, float targetX, float targetY, float alpha) { + float parentRotation = (!bone.data.inheritRotation || bone.parent == null) ? 0 : bone.parent.worldRotation; + float rotation = bone.rotation; + float rotationIK = (float)Math.Atan2(targetY - bone.worldY, targetX - bone.worldX) * radDeg; + if (bone.worldFlipX != (bone.worldFlipY != Bone.yDown)) rotationIK = -rotationIK; + rotationIK -= parentRotation; + bone.rotationIK = rotation + (rotationIK - rotation) * alpha; + } + + /// Adjusts the parent and child bone rotations so the tip of the child is as close to the target position as + /// possible. The target is specified in the world coordinate system. + /// Any descendant bone of the parent. + static public void apply (Bone parent, Bone child, float targetX, float targetY, int bendDirection, float alpha) { + float childRotation = child.rotation, parentRotation = parent.rotation; + if (alpha == 0) { + child.rotationIK = childRotation; + parent.rotationIK = parentRotation; + return; + } + float positionX, positionY; + Bone parentParent = parent.parent; + if (parentParent != null) { + parentParent.worldToLocal(targetX, targetY, out positionX, out positionY); + targetX = (positionX - parent.x) * parentParent.worldScaleX; + targetY = (positionY - parent.y) * parentParent.worldScaleY; + } else { + targetX -= parent.x; + targetY -= parent.y; + } + if (child.parent == parent) { + positionX = child.x; + positionY = child.y; + } else { + child.parent.localToWorld(child.x, child.y, out positionX, out positionY); + parent.worldToLocal(positionX, positionY, out positionX, out positionY); + } + float childX = positionX * parent.worldScaleX, childY = positionY * parent.worldScaleY; + float offset = (float)Math.Atan2(childY, childX); + float len1 = (float)Math.Sqrt(childX * childX + childY * childY), len2 = child.data.length * child.worldScaleX; + // Based on code by Ryan Juckett with permission: Copyright (c) 2008-2009 Ryan Juckett, http://www.ryanjuckett.com/ + float cosDenom = 2 * len1 * len2; + if (cosDenom < 0.0001f) { + child.rotationIK = childRotation + ((float)Math.Atan2(targetY, targetX) * radDeg - parentRotation - childRotation) + * alpha; + return; + } + float cos = (targetX * targetX + targetY * targetY - len1 * len1 - len2 * len2) / cosDenom; + if (cos < -1) + cos = -1; + else if (cos > 1) + cos = 1; + float childAngle = (float)Math.Acos(cos) * bendDirection; + float adjacent = len1 + len2 * cos, opposite = len2 * (float)Math.Sin(childAngle); + float parentAngle = (float)Math.Atan2(targetY * adjacent - targetX * opposite, targetX * adjacent + targetY * opposite); + float rotation = (parentAngle - offset) * radDeg - parentRotation; + if (rotation > 180) + rotation -= 360; + else if (rotation < -180) // + rotation += 360; + parent.rotationIK = parentRotation + rotation * alpha; + rotation = (childAngle + offset) * radDeg - childRotation; + if (rotation > 180) + rotation -= 360; + else if (rotation < -180) // + rotation += 360; + child.rotationIK = childRotation + (rotation + parent.worldRotation - child.parent.worldRotation) * alpha; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/IkConstraintData.cs b/SpineRuntimes/SpineRuntime21/IkConstraintData.cs new file mode 100644 index 0000000..ccbcb41 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/IkConstraintData.cs @@ -0,0 +1,57 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class IkConstraintData { + internal String name; + internal List bones = new List(); + internal BoneData target; + internal int bendDirection = 1; + internal float mix = 1; + + public String Name { get { return name; } } + public List Bones { get { return bones; } } + public BoneData Target { get { return target; } set { target = value; } } + public int BendDirection { get { return bendDirection; } set { bendDirection = value; } } + public float Mix { get { return mix; } set { mix = value; } } + + public IkConstraintData (String name) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + this.name = name; + } + + override public String ToString () { + return name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Json.cs b/SpineRuntimes/SpineRuntime21/Json.cs new file mode 100644 index 0000000..07e3ae2 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Json.cs @@ -0,0 +1,542 @@ +/* + * Copyright (c) 2012 Calvin Rien + * + * Based on the JSON parser by Patrick van Bergen + * http://techblog.procurios.nl/k/618/news/view/14605/14863/How-do-I-write-my-own-parser-for-JSON.html + * + * Simplified it so that it doesn't throw exceptions + * and can be used in Unity iPhone with maximum code stripping. + * + * Permission is hereby granted, free of charge, to any person obtaining + * a copy of this software and associated documentation files (the + * "Software"), to deal in the Software without restriction, including + * without limitation the rights to use, copy, modify, merge, publish, + * distribute, sublicense, and/or sell copies of the Software, and to + * permit persons to whom the Software is furnished to do so, subject to + * the following conditions: + * + * The above copyright notice and this permission notice shall be + * included in all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, + * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF + * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. + * IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY + * CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, + * TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE + * SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + */ +using System; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Globalization; + +namespace SpineRuntime21 +{ + // Example usage: + // + // using UnityEngine; + // using System.Collections; + // using System.Collections.Generic; + // using MiniJSON; + // + // public class MiniJSONTest : MonoBehaviour { + // void Start () { + // var jsonString = "{ \"array\": [1.44,2,3], " + + // "\"object\": {\"key1\":\"value1\", \"key2\":256}, " + + // "\"string\": \"The quick brown fox \\\"jumps\\\" over the lazy dog \", " + + // "\"unicode\": \"\\u3041 Men\u00fa sesi\u00f3n\", " + + // "\"int\": 65536, " + + // "\"float\": 3.1415926, " + + // "\"bool\": true, " + + // "\"null\": null }"; + // + // var dict = Json.Deserialize(jsonString) as Dictionary; + // + // Debug.Log("deserialized: " + dict.GetType()); + // Debug.Log("dict['array'][0]: " + ((List) dict["array"])[0]); + // Debug.Log("dict['string']: " + (string) dict["string"]); + // Debug.Log("dict['float']: " + (float) dict["float"]); + // Debug.Log("dict['int']: " + (long) dict["int"]); // ints come out as longs + // Debug.Log("dict['unicode']: " + (string) dict["unicode"]); + // + // var str = Json.Serialize(dict); + // + // Debug.Log("serialized: " + str); + // } + // } + + /// + /// This class encodes and decodes JSON strings. + /// Spec. details, see http://www.json.org/ + /// + /// JSON uses Arrays and Objects. These correspond here to the datatypes IList and IDictionary. + /// All numbers are parsed to floats. + /// + public static class Json { + /// + /// Parses the string json into a value + /// + /// A JSON string. + /// An List<object>, a Dictionary<string, object>, a float, an integer,a string, null, true, or false + public static object Deserialize (TextReader json) { + if (json == null) { + return null; + } + return Parser.Parse(json); + } + + sealed class Parser : IDisposable { + const string WHITE_SPACE = " \t\n\r"; + const string WORD_BREAK = " \t\n\r{}[],:\""; + + enum TOKEN { + NONE, + CURLY_OPEN, + CURLY_CLOSE, + SQUARED_OPEN, + SQUARED_CLOSE, + COLON, + COMMA, + STRING, + NUMBER, + TRUE, + FALSE, + NULL + }; + + TextReader json; + + Parser(TextReader reader) { + json = reader; + } + + public static object Parse (TextReader reader) { + using (var instance = new Parser(reader)) { + return instance.ParseValue(); + } + } + + public void Dispose() { + json.Dispose(); + json = null; + } + + Dictionary ParseObject() { + Dictionary table = new Dictionary(); + + // ditch opening brace + json.Read(); + + // { + while (true) { + switch (NextToken) { + case TOKEN.NONE: + return null; + case TOKEN.COMMA: + continue; + case TOKEN.CURLY_CLOSE: + return table; + default: + // name + string name = ParseString(); + if (name == null) { + return null; + } + + // : + if (NextToken != TOKEN.COLON) { + return null; + } + // ditch the colon + json.Read(); + + // value + table[name] = ParseValue(); + break; + } + } + } + + List ParseArray() { + List array = new List(); + + // ditch opening bracket + json.Read(); + + // [ + var parsing = true; + while (parsing) { + TOKEN nextToken = NextToken; + + switch (nextToken) { + case TOKEN.NONE: + return null; + case TOKEN.COMMA: + continue; + case TOKEN.SQUARED_CLOSE: + parsing = false; + break; + default: + object value = ParseByToken(nextToken); + + array.Add(value); + break; + } + } + + return array; + } + + object ParseValue() { + TOKEN nextToken = NextToken; + return ParseByToken(nextToken); + } + + object ParseByToken(TOKEN token) { + switch (token) { + case TOKEN.STRING: + return ParseString(); + case TOKEN.NUMBER: + return ParseNumber(); + case TOKEN.CURLY_OPEN: + return ParseObject(); + case TOKEN.SQUARED_OPEN: + return ParseArray(); + case TOKEN.TRUE: + return true; + case TOKEN.FALSE: + return false; + case TOKEN.NULL: + return null; + default: + return null; + } + } + + string ParseString() { + StringBuilder s = new StringBuilder(); + char c; + + // ditch opening quote + json.Read(); + + bool parsing = true; + while (parsing) { + + if (json.Peek() == -1) { + parsing = false; + break; + } + + c = NextChar; + switch (c) { + case '"': + parsing = false; + break; + case '\\': + if (json.Peek() == -1) { + parsing = false; + break; + } + + c = NextChar; + switch (c) { + case '"': + case '\\': + case '/': + s.Append(c); + break; + case 'b': + s.Append('\b'); + break; + case 'f': + s.Append('\f'); + break; + case 'n': + s.Append('\n'); + break; + case 'r': + s.Append('\r'); + break; + case 't': + s.Append('\t'); + break; + case 'u': + var hex = new StringBuilder(); + + for (int i=0; i< 4; i++) { + hex.Append(NextChar); + } + + s.Append((char) Convert.ToInt32(hex.ToString(), 16)); + break; + } + break; + default: + s.Append(c); + break; + } + } + + return s.ToString(); + } + + object ParseNumber() { + string number = NextWord; + float parsedFloat; + float.TryParse(number, NumberStyles.Float, CultureInfo.InvariantCulture, out parsedFloat); + return parsedFloat; + } + + void EatWhitespace() { + while (WHITE_SPACE.IndexOf(PeekChar) != -1) { + json.Read(); + + if (json.Peek() == -1) { + break; + } + } + } + + char PeekChar { + get { + return Convert.ToChar(json.Peek()); + } + } + + char NextChar { + get { + return Convert.ToChar(json.Read()); + } + } + + string NextWord { + get { + StringBuilder word = new StringBuilder(); + + while (WORD_BREAK.IndexOf(PeekChar) == -1) { + word.Append(NextChar); + + if (json.Peek() == -1) { + break; + } + } + + return word.ToString(); + } + } + + TOKEN NextToken { + get { + EatWhitespace(); + + if (json.Peek() == -1) { + return TOKEN.NONE; + } + + char c = PeekChar; + switch (c) { + case '{': + return TOKEN.CURLY_OPEN; + case '}': + json.Read(); + return TOKEN.CURLY_CLOSE; + case '[': + return TOKEN.SQUARED_OPEN; + case ']': + json.Read(); + return TOKEN.SQUARED_CLOSE; + case ',': + json.Read(); + return TOKEN.COMMA; + case '"': + return TOKEN.STRING; + case ':': + return TOKEN.COLON; + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + case '-': + return TOKEN.NUMBER; + } + + string word = NextWord; + + switch (word) { + case "false": + return TOKEN.FALSE; + case "true": + return TOKEN.TRUE; + case "null": + return TOKEN.NULL; + } + + return TOKEN.NONE; + } + } + } + + /// + /// Converts a IDictionary / IList object or a simple type (string, int, etc.) into a JSON string + /// + /// A Dictionary<string, object> / List<object> + /// A JSON encoded string, or null if object 'json' is not serializable + public static string Serialize(object obj) { + return Serializer.Serialize(obj); + } + + sealed class Serializer { + StringBuilder builder; + + Serializer() { + builder = new StringBuilder(); + } + + public static string Serialize(object obj) { + var instance = new Serializer(); + + instance.SerializeValue(obj); + + return instance.builder.ToString(); + } + + void SerializeValue(object value) { + IList asList; + IDictionary asDict; + string asStr; + + if (value == null) { + builder.Append("null"); + } + else if ((asStr = value as string) != null) { + SerializeString(asStr); + } + else if (value is bool) { + builder.Append(value.ToString().ToLower()); + } + else if ((asList = value as IList) != null) { + SerializeArray(asList); + } + else if ((asDict = value as IDictionary) != null) { + SerializeObject(asDict); + } + else if (value is char) { + SerializeString(value.ToString()); + } + else { + SerializeOther(value); + } + } + + void SerializeObject(IDictionary obj) { + bool first = true; + + builder.Append('{'); + + foreach (object e in obj.Keys) { + if (!first) { + builder.Append(','); + } + + SerializeString(e.ToString()); + builder.Append(':'); + + SerializeValue(obj[e]); + + first = false; + } + + builder.Append('}'); + } + + void SerializeArray(IList anArray) { + builder.Append('['); + + bool first = true; + + foreach (object obj in anArray) { + if (!first) { + builder.Append(','); + } + + SerializeValue(obj); + + first = false; + } + + builder.Append(']'); + } + + void SerializeString(string str) { + builder.Append('\"'); + + char[] charArray = str.ToCharArray(); + foreach (var c in charArray) { + switch (c) { + case '"': + builder.Append("\\\""); + break; + case '\\': + builder.Append("\\\\"); + break; + case '\b': + builder.Append("\\b"); + break; + case '\f': + builder.Append("\\f"); + break; + case '\n': + builder.Append("\\n"); + break; + case '\r': + builder.Append("\\r"); + break; + case '\t': + builder.Append("\\t"); + break; + default: + int codepoint = Convert.ToInt32(c); + if ((codepoint >= 32) && (codepoint <= 126)) { + builder.Append(c); + } + else { + builder.Append("\\u" + Convert.ToString(codepoint, 16).PadLeft(4, '0')); + } + break; + } + } + + builder.Append('\"'); + } + + void SerializeOther(object value) { + if (value is float + || value is int + || value is uint + || value is long + || value is float + || value is sbyte + || value is byte + || value is short + || value is ushort + || value is ulong + || value is decimal) { + builder.Append(value.ToString()); + } + else { + SerializeString(value.ToString()); + } + } + } + } +} \ No newline at end of file diff --git a/SpineRuntimes/SpineRuntime21/Skeleton.cs b/SpineRuntimes/SpineRuntime21/Skeleton.cs new file mode 100644 index 0000000..cb9f382 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Skeleton.cs @@ -0,0 +1,306 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class Skeleton { + internal SkeletonData data; + internal List bones; + internal List slots; + internal List drawOrder; + internal List ikConstraints; + private List> boneCache = new List>(); + internal Skin skin; + internal float r = 1, g = 1, b = 1, a = 1; + internal float time; + internal bool flipX, flipY; + internal float x, y; + + public SkeletonData Data { get { return data; } } + public List Bones { get { return bones; } } + public List Slots { get { return slots; } } + public List DrawOrder { get { return drawOrder; } } + public List IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } } + public Skin Skin { get { return skin; } set { skin = value; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + public float Time { get { return time; } set { time = value; } } + public float X { get { return x; } set { x = value; } } + public float Y { get { return y; } set { y = value; } } + public bool FlipX { get { return flipX; } set { flipX = value; } } + public bool FlipY { get { return flipY; } set { flipY = value; } } + + public Bone RootBone { + get { + return bones.Count == 0 ? null : bones[0]; + } + } + + public Skeleton (SkeletonData data) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + this.data = data; + + bones = new List(data.bones.Count); + foreach (BoneData boneData in data.bones) { + Bone parent = boneData.parent == null ? null : bones[data.bones.IndexOf(boneData.parent)]; + Bone bone = new Bone(boneData, this, parent); + if (parent != null) parent.children.Add(bone); + bones.Add(bone); + } + + slots = new List(data.slots.Count); + drawOrder = new List(data.slots.Count); + foreach (SlotData slotData in data.slots) { + Bone bone = bones[data.bones.IndexOf(slotData.boneData)]; + Slot slot = new Slot(slotData, bone); + slots.Add(slot); + drawOrder.Add(slot); + } + + ikConstraints = new List(data.ikConstraints.Count); + foreach (IkConstraintData ikConstraintData in data.ikConstraints) + ikConstraints.Add(new IkConstraint(ikConstraintData, this)); + + UpdateCache(); + } + + /// Caches information about bones and IK constraints. Must be called if bones or IK constraints are added or + /// removed. + public void UpdateCache () { + List> boneCache = this.boneCache; + List ikConstraints = this.ikConstraints; + int ikConstraintsCount = ikConstraints.Count; + + int arrayCount = ikConstraintsCount + 1; + if (boneCache.Count > arrayCount) boneCache.RemoveRange(arrayCount, boneCache.Count - arrayCount); + for (int i = 0, n = boneCache.Count; i < n; i++) + boneCache[i].Clear(); + while (boneCache.Count < arrayCount) + boneCache.Add(new List()); + + List nonIkBones = boneCache[0]; + + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bones[i]; + Bone current = bone; + do { + for (int ii = 0; ii < ikConstraintsCount; ii++) { + IkConstraint ikConstraint = ikConstraints[ii]; + Bone parent = ikConstraint.bones[0]; + Bone child = ikConstraint.bones[ikConstraint.bones.Count - 1]; + while (true) { + if (current == child) { + boneCache[ii].Add(bone); + boneCache[ii + 1].Add(bone); + goto outer; + } + if (child == parent) break; + child = child.parent; + } + } + current = current.parent; + } while (current != null); + nonIkBones.Add(bone); + outer: {} + } + } + + /// Updates the world transform for each bone and applies IK constraints. + public void UpdateWorldTransform () { + List bones = this.bones; + for (int ii = 0, nn = bones.Count; ii < nn; ii++) { + Bone bone = bones[ii]; + bone.rotationIK = bone.rotation; + } + List> boneCache = this.boneCache; + List ikConstraints = this.ikConstraints; + int i = 0, last = boneCache.Count - 1; + while (true) { + List updateBones = boneCache[i]; + for (int ii = 0, nn = updateBones.Count; ii < nn; ii++) + updateBones[ii].UpdateWorldTransform(); + if (i == last) break; + ikConstraints[i].apply(); + i++; + } + } + + /// Sets the bones and slots to their setup pose values. + public void SetToSetupPose () { + SetBonesToSetupPose(); + SetSlotsToSetupPose(); + } + + public void SetBonesToSetupPose () { + List bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) + bones[i].SetToSetupPose(); + + List ikConstraints = this.ikConstraints; + for (int i = 0, n = ikConstraints.Count; i < n; i++) { + IkConstraint ikConstraint = ikConstraints[i]; + ikConstraint.bendDirection = ikConstraint.data.bendDirection; + ikConstraint.mix = ikConstraint.data.mix; + } + } + + public void SetSlotsToSetupPose () { + List slots = this.slots; + drawOrder.Clear(); + drawOrder.AddRange(slots); + for (int i = 0, n = slots.Count; i < n; i++) + slots[i].SetToSetupPose(i); + } + + /// May be null. + public Bone FindBone (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + List bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) { + Bone bone = bones[i]; + if (bone.data.name == boneName) return bone; + } + return null; + } + + /// -1 if the bone was not found. + public int FindBoneIndex (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + List bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) + if (bones[i].data.name == boneName) return i; + return -1; + } + + /// May be null. + public Slot FindSlot (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.data.name == slotName) return slot; + } + return null; + } + + /// -1 if the bone was not found. + public int FindSlotIndex (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) + if (slots[i].data.name.Equals(slotName)) return i; + return -1; + } + + /// Sets a skin by name (see SetSkin). + public void SetSkin (String skinName) { + Skin skin = data.FindSkin(skinName); + if (skin == null) throw new ArgumentException("Skin not found: " + skinName); + SetSkin(skin); + } + + /// Sets the skin used to look up attachments before looking in the {@link SkeletonData#getDefaultSkin() default + /// skin}. Attachmentsfrom the new skin are attached if the corresponding attachment from the old skin was attached. If + /// there was no old skin, each slot's setup mode attachment is attached from the new skin. + /// May be null. + public void SetSkin (Skin newSkin) { + if (newSkin != null) { + if (skin != null) + newSkin.AttachAll(this, skin); + else { + List slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots[i]; + String name = slot.data.attachmentName; + if (name != null) { + Attachment attachment = newSkin.GetAttachment(i, name); + if (attachment != null) slot.Attachment = attachment; + } + } + } + } + skin = newSkin; + } + + /// May be null. + public Attachment GetAttachment (String slotName, String attachmentName) { + return GetAttachment(data.FindSlotIndex(slotName), attachmentName); + } + + /// May be null. + public Attachment GetAttachment (int slotIndex, String attachmentName) { + if (attachmentName == null) throw new ArgumentNullException("attachmentName cannot be null."); + if (skin != null) { + Attachment attachment = skin.GetAttachment(slotIndex, attachmentName); + if (attachment != null) return attachment; + } + if (data.defaultSkin != null) return data.defaultSkin.GetAttachment(slotIndex, attachmentName); + return null; + } + + /// May be null. + public void SetAttachment (String slotName, String attachmentName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + Slot slot = slots[i]; + if (slot.data.name == slotName) { + Attachment attachment = null; + if (attachmentName != null) { + attachment = GetAttachment(i, attachmentName); + if (attachment == null) throw new Exception("Attachment not found: " + attachmentName + ", for slot: " + slotName); + } + slot.Attachment = attachment; + return; + } + } + throw new Exception("Slot not found: " + slotName); + } + + /** @return May be null. */ + public IkConstraint FindIkConstraint (String ikConstraintName) { + if (ikConstraintName == null) throw new ArgumentNullException("ikConstraintName cannot be null."); + List ikConstraints = this.ikConstraints; + for (int i = 0, n = ikConstraints.Count; i < n; i++) { + IkConstraint ikConstraint = ikConstraints[i]; + if (ikConstraint.data.name == ikConstraintName) return ikConstraint; + } + return null; + } + + public void Update (float delta) { + time += delta; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs b/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs new file mode 100644 index 0000000..633b0c6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/SkeletonBinary.cs @@ -0,0 +1,663 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.IO; +using System.Collections.Generic; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace SpineRuntime21 { + public class SkeletonBinary { + public const int TIMELINE_SCALE = 0; + public const int TIMELINE_ROTATE = 1; + public const int TIMELINE_TRANSLATE = 2; + public const int TIMELINE_ATTACHMENT = 3; + public const int TIMELINE_COLOR = 4; + public const int TIMELINE_FLIPX = 5; + public const int TIMELINE_FLIPY = 6; + + public const int CURVE_LINEAR = 0; + public const int CURVE_STEPPED = 1; + public const int CURVE_BEZIER = 2; + + private AttachmentLoader attachmentLoader; + public float Scale { get; set; } + private char[] chars = new char[32]; + private byte[] buffer = new byte[4]; + + public SkeletonBinary (params Atlas[] atlasArray) + : this(new AtlasAttachmentLoader(atlasArray)) { + } + + public SkeletonBinary (AttachmentLoader attachmentLoader) { + if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader cannot be null."); + this.attachmentLoader = attachmentLoader; + Scale = 1; + } + +#if WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + using (var input = new BufferedStream(await folder.GetFileAsync(path).AsTask().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public SkeletonData ReadSkeletonData (String path) { + return this.ReadFile(path).Result; + } +#else + public SkeletonData ReadSkeletonData (String path) { +#if WINDOWS_PHONE + using (var input = new BufferedStream(Microsoft.Xna.Framework.TitleContainer.OpenStream(path))) + { +#else + using (var input = new BufferedStream(new FileStream(path, FileMode.Open))) { +#endif + SkeletonData skeletonData = ReadSkeletonData(input); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } +#endif + + public SkeletonData ReadSkeletonData (Stream input) { + if (input == null) throw new ArgumentNullException("input cannot be null."); + float scale = Scale; + + var skeletonData = new SkeletonData(); + skeletonData.hash = ReadString(input); + if (skeletonData.hash.Length == 0) skeletonData.hash = null; + skeletonData.version = ReadString(input); + if (skeletonData.version.Length == 0) skeletonData.version = null; + skeletonData.width = ReadFloat(input); + skeletonData.height = ReadFloat(input); + + bool nonessential = ReadBoolean(input); + + if (nonessential) { + skeletonData.imagesPath = ReadString(input); + if (skeletonData.imagesPath.Length == 0) skeletonData.imagesPath = null; + } + + // Bones. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + String name = ReadString(input); + BoneData parent = null; + int parentIndex = ReadInt(input, true) - 1; + if (parentIndex != -1) parent = skeletonData.bones[parentIndex]; + BoneData boneData = new BoneData(name, parent); + boneData.x = ReadFloat(input) * scale; + boneData.y = ReadFloat(input) * scale; + boneData.scaleX = ReadFloat(input); + boneData.scaleY = ReadFloat(input); + boneData.rotation = ReadFloat(input); + boneData.length = ReadFloat(input) * scale; + boneData.flipX = ReadBoolean(input); + boneData.flipY = ReadBoolean(input); + boneData.inheritScale = ReadBoolean(input); + boneData.inheritRotation = ReadBoolean(input); + if (nonessential) ReadInt(input); // Skip bone color. + skeletonData.bones.Add(boneData); + } + + // IK constraints. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + IkConstraintData ikConstraintData = new IkConstraintData(ReadString(input)); + for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) + ikConstraintData.bones.Add(skeletonData.bones[ReadInt(input, true)]); + ikConstraintData.target = skeletonData.bones[ReadInt(input, true)]; + ikConstraintData.mix = ReadFloat(input); + ikConstraintData.bendDirection = ReadSByte(input); + skeletonData.ikConstraints.Add(ikConstraintData); + } + + // Slots. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + String slotName = ReadString(input); + BoneData boneData = skeletonData.bones[ReadInt(input, true)]; + SlotData slotData = new SlotData(slotName, boneData); + int color = ReadInt(input); + slotData.r = ((color & 0xff000000) >> 24) / 255f; + slotData.g = ((color & 0x00ff0000) >> 16) / 255f; + slotData.b = ((color & 0x0000ff00) >> 8) / 255f; + slotData.a = ((color & 0x000000ff)) / 255f; + slotData.attachmentName = ReadString(input); + slotData.additiveBlending = ReadBoolean(input); + skeletonData.slots.Add(slotData); + } + + // Default skin. + Skin defaultSkin = ReadSkin(input, "default", nonessential); + if (defaultSkin != null) { + skeletonData.defaultSkin = defaultSkin; + skeletonData.skins.Add(defaultSkin); + } + + // Skins. + for (int i = 0, n = ReadInt(input, true); i < n; i++) + skeletonData.skins.Add(ReadSkin(input, ReadString(input), nonessential)); + + // Events. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + EventData eventData = new EventData(ReadString(input)); + eventData.Int = ReadInt(input, false); + eventData.Float = ReadFloat(input); + eventData.String = ReadString(input); + skeletonData.events.Add(eventData); + } + + // Animations. + for (int i = 0, n = ReadInt(input, true); i < n; i++) + ReadAnimation(ReadString(input), input, skeletonData); + + skeletonData.bones.TrimExcess(); + skeletonData.slots.TrimExcess(); + skeletonData.skins.TrimExcess(); + skeletonData.events.TrimExcess(); + skeletonData.animations.TrimExcess(); + skeletonData.ikConstraints.TrimExcess(); + return skeletonData; + } + + /** @return May be null. */ + private Skin ReadSkin (Stream input, String skinName, bool nonessential) { + int slotCount = ReadInt(input, true); + if (slotCount == 0) return null; + Skin skin = new Skin(skinName); + for (int i = 0; i < slotCount; i++) { + int slotIndex = ReadInt(input, true); + for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) { + String name = ReadString(input); + skin.AddAttachment(slotIndex, name, ReadAttachment(input, skin, name, nonessential)); + } + } + return skin; + } + + private Attachment ReadAttachment (Stream input, Skin skin, String attachmentName, bool nonessential) { + float scale = Scale; + + String name = ReadString(input); + if (name == null) name = attachmentName; + + switch ((AttachmentType)input.ReadByte()) { + case AttachmentType.region: { + String path = ReadString(input); + if (path == null) path = name; + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path); + if (region == null) return null; + region.Path = path; + region.x = ReadFloat(input) * scale; + region.y = ReadFloat(input) * scale; + region.scaleX = ReadFloat(input); + region.scaleY = ReadFloat(input); + region.rotation = ReadFloat(input); + region.width = ReadFloat(input) * scale; + region.height = ReadFloat(input) * scale; + int color = ReadInt(input); + region.r = ((color & 0xff000000) >> 24) / 255f; + region.g = ((color & 0x00ff0000) >> 16) / 255f; + region.b = ((color & 0x0000ff00) >> 8) / 255f; + region.a = ((color & 0x000000ff)) / 255f; + region.UpdateOffset(); + return region; + } + case AttachmentType.boundingbox: { + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.vertices = ReadFloatArray(input, scale); + return box; + } + case AttachmentType.mesh: { + String path = ReadString(input); + if (path == null) path = name; + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + mesh.regionUVs = ReadFloatArray(input, 1); + mesh.triangles = ReadShortArray(input); + mesh.vertices = ReadFloatArray(input, scale); + mesh.UpdateUVs(); + int color = ReadInt(input); + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.HullLength = ReadInt(input, true) * 2; + if (nonessential) { + mesh.Edges = ReadIntArray(input); + mesh.Width = ReadFloat(input) * scale; + mesh.Height = ReadFloat(input) * scale; + } + return mesh; + } + case AttachmentType.skinnedmesh: { + String path = ReadString(input); + if (path == null) path = name; + SkinnedMeshAttachment mesh = attachmentLoader.NewSkinnedMeshAttachment(skin, name, path); + if (mesh == null) return null; + mesh.Path = path; + float[] uvs = ReadFloatArray(input, 1); + int[] triangles = ReadShortArray(input); + + int vertexCount = ReadInt(input, true); + var weights = new List(uvs.Length * 3 * 3); + var bones = new List(uvs.Length * 3); + for (int i = 0; i < vertexCount; i++) { + int boneCount = (int)ReadFloat(input); + bones.Add(boneCount); + for (int nn = i + boneCount * 4; i < nn; i += 4) { + bones.Add((int)ReadFloat(input)); + weights.Add(ReadFloat(input) * scale); + weights.Add(ReadFloat(input) * scale); + weights.Add(ReadFloat(input)); + } + } + mesh.bones = bones.ToArray(); + mesh.weights = weights.ToArray(); + mesh.triangles = triangles; + mesh.regionUVs = uvs; + mesh.UpdateUVs(); + int color = ReadInt(input); + mesh.r = ((color & 0xff000000) >> 24) / 255f; + mesh.g = ((color & 0x00ff0000) >> 16) / 255f; + mesh.b = ((color & 0x0000ff00) >> 8) / 255f; + mesh.a = ((color & 0x000000ff)) / 255f; + mesh.HullLength = ReadInt(input, true) * 2; + if (nonessential) { + mesh.Edges = ReadIntArray(input); + mesh.Width = ReadFloat(input) * scale; + mesh.Height = ReadFloat(input) * scale; + } + return mesh; + } + } + return null; + } + + private float[] ReadFloatArray (Stream input, float scale) { + int n = ReadInt(input, true); + float[] array = new float[n]; + if (scale == 1) { + for (int i = 0; i < n; i++) + array[i] = ReadFloat(input); + } else { + for (int i = 0; i < n; i++) + array[i] = ReadFloat(input) * scale; + } + return array; + } + + private int[] ReadShortArray (Stream input) { + int n = ReadInt(input, true); + int[] array = new int[n]; + for (int i = 0; i < n; i++) + array[i] = (input.ReadByte() << 8) + input.ReadByte(); + return array; + } + + private int[] ReadIntArray (Stream input) { + int n = ReadInt(input, true); + int[] array = new int[n]; + for (int i = 0; i < n; i++) + array[i] = ReadInt(input, true); + return array; + } + + private void ReadAnimation (String name, Stream input, SkeletonData skeletonData) { + var timelines = new List(); + float scale = Scale; + float duration = 0; + + // Slot timelines. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + int slotIndex = ReadInt(input, true); + for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) { + int timelineType = input.ReadByte(); + int frameCount = ReadInt(input, true); + switch (timelineType) { + case TIMELINE_COLOR: { + ColorTimeline timeline = new ColorTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + float time = ReadFloat(input); + int color = ReadInt(input); + float r = ((color & 0xff000000) >> 24) / 255f; + float g = ((color & 0x00ff0000) >> 16) / 255f; + float b = ((color & 0x0000ff00) >> 8) / 255f; + float a = ((color & 0x000000ff)) / 255f; + timeline.SetFrame(frameIndex, time, r, g, b, a); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount * 5 - 5]); + break; + } + case TIMELINE_ATTACHMENT: { + AttachmentTimeline timeline = new AttachmentTimeline(frameCount); + timeline.slotIndex = slotIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) + timeline.SetFrame(frameIndex, ReadFloat(input), ReadString(input)); + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount - 1]); + break; + } + } + } + } + + // Bone timelines. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + int boneIndex = ReadInt(input, true); + for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) { + int timelineType = input.ReadByte(); + int frameCount = ReadInt(input, true); + switch (timelineType) { + case TIMELINE_ROTATE: { + RotateTimeline timeline = new RotateTimeline(frameCount); + timeline.boneIndex = boneIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input)); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount * 2 - 2]); + break; + } + case TIMELINE_TRANSLATE: + case TIMELINE_SCALE: { + TranslateTimeline timeline; + float timelineScale = 1; + if (timelineType == TIMELINE_SCALE) + timeline = new ScaleTimeline(frameCount); + else { + timeline = new TranslateTimeline(frameCount); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input) * timelineScale, ReadFloat(input) + * timelineScale); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount * 3 - 3]); + break; + } + case TIMELINE_FLIPX: + case TIMELINE_FLIPY: { + FlipXTimeline timeline = timelineType == TIMELINE_FLIPX ? new FlipXTimeline(frameCount) : new FlipYTimeline( + frameCount); + timeline.boneIndex = boneIndex; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) + timeline.SetFrame(frameIndex, ReadFloat(input), ReadBoolean(input)); + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount * 2 - 2]); + break; + } + } + } + } + + // IK timelines. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + IkConstraintData ikConstraint = skeletonData.ikConstraints[ReadInt(input, true)]; + int frameCount = ReadInt(input, true); + IkConstraintTimeline timeline = new IkConstraintTimeline(frameCount); + timeline.ikConstraintIndex = skeletonData.ikConstraints.IndexOf(ikConstraint); + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + timeline.SetFrame(frameIndex, ReadFloat(input), ReadFloat(input), ReadSByte(input)); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount * 3 - 3]); + } + + // FFD timelines. + for (int i = 0, n = ReadInt(input, true); i < n; i++) { + Skin skin = skeletonData.skins[ReadInt(input, true)]; + for (int ii = 0, nn = ReadInt(input, true); ii < nn; ii++) { + int slotIndex = ReadInt(input, true); + for (int iii = 0, nnn = ReadInt(input, true); iii < nnn; iii++) { + Attachment attachment = skin.GetAttachment(slotIndex, ReadString(input)); + int frameCount = ReadInt(input, true); + FFDTimeline timeline = new FFDTimeline(frameCount); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + for (int frameIndex = 0; frameIndex < frameCount; frameIndex++) { + float time = ReadFloat(input); + + float[] vertices; + int vertexCount; + if (attachment is MeshAttachment) + vertexCount = ((MeshAttachment)attachment).vertices.Length; + else + vertexCount = ((SkinnedMeshAttachment)attachment).weights.Length / 3 * 2; + + int end = ReadInt(input, true); + if (end == 0) { + if (attachment is MeshAttachment) + vertices = ((MeshAttachment)attachment).vertices; + else + vertices = new float[vertexCount]; + } else { + vertices = new float[vertexCount]; + int start = ReadInt(input, true); + end += start; + if (scale == 1) { + for (int v = start; v < end; v++) + vertices[v] = ReadFloat(input); + } else { + for (int v = start; v < end; v++) + vertices[v] = ReadFloat(input) * scale; + } + if (attachment is MeshAttachment) { + float[] meshVertices = ((MeshAttachment)attachment).vertices; + for (int v = 0, vn = vertices.Length; v < vn; v++) + vertices[v] += meshVertices[v]; + } + } + + timeline.SetFrame(frameIndex, time, vertices); + if (frameIndex < frameCount - 1) ReadCurve(input, frameIndex, timeline); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[frameCount - 1]); + } + } + } + + // Draw order timeline. + int drawOrderCount = ReadInt(input, true); + if (drawOrderCount > 0) { + DrawOrderTimeline timeline = new DrawOrderTimeline(drawOrderCount); + int slotCount = skeletonData.slots.Count; + for (int i = 0; i < drawOrderCount; i++) { + int offsetCount = ReadInt(input, true); + int[] drawOrder = new int[slotCount]; + for (int ii = slotCount - 1; ii >= 0; ii--) + drawOrder[ii] = -1; + int[] unchanged = new int[slotCount - offsetCount]; + int originalIndex = 0, unchangedIndex = 0; + for (int ii = 0; ii < offsetCount; ii++) { + int slotIndex = ReadInt(input, true); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + drawOrder[originalIndex + ReadInt(input, true)] = originalIndex++; + } + // Collect remaining unchanged items. + while (originalIndex < slotCount) + unchanged[unchangedIndex++] = originalIndex++; + // Fill in unchanged items. + for (int ii = slotCount - 1; ii >= 0; ii--) + if (drawOrder[ii] == -1) drawOrder[ii] = unchanged[--unchangedIndex]; + timeline.SetFrame(i, ReadFloat(input), drawOrder); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[drawOrderCount - 1]); + } + + // Event timeline. + int eventCount = ReadInt(input, true); + if (eventCount > 0) { + EventTimeline timeline = new EventTimeline(eventCount); + for (int i = 0; i < eventCount; i++) { + float time = ReadFloat(input); + EventData eventData = skeletonData.events[ReadInt(input, true)]; + Event e = new Event(eventData); + e.Int = ReadInt(input, false); + e.Float = ReadFloat(input); + e.String = ReadBoolean(input) ? ReadString(input) : eventData.String; + timeline.SetFrame(i, time, e); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[eventCount - 1]); + } + + timelines.TrimExcess(); + skeletonData.animations.Add(new Animation(name, timelines, duration)); + } + + private void ReadCurve (Stream input, int frameIndex, CurveTimeline timeline) { + switch (input.ReadByte()) { + case CURVE_STEPPED: + timeline.SetStepped(frameIndex); + break; + case CURVE_BEZIER: + timeline.SetCurve(frameIndex, ReadFloat(input), ReadFloat(input), ReadFloat(input), ReadFloat(input)); + break; + } + } + + private sbyte ReadSByte (Stream input) { + int value = input.ReadByte(); + if (value == -1) throw new EndOfStreamException(); + return (sbyte)value; + } + + private bool ReadBoolean (Stream input) { + return input.ReadByte() != 0; + } + + private float ReadFloat (Stream input) { + buffer[3] = (byte)input.ReadByte(); + buffer[2] = (byte)input.ReadByte(); + buffer[1] = (byte)input.ReadByte(); + buffer[0] = (byte)input.ReadByte(); + return BitConverter.ToSingle(buffer, 0); + } + + private int ReadInt (Stream input) { + return (input.ReadByte() << 24) + (input.ReadByte() << 16) + (input.ReadByte() << 8) + input.ReadByte(); + } + + private int ReadInt (Stream input, bool optimizePositive) { + int b = input.ReadByte(); + int result = b & 0x7F; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 7; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 14; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 21; + if ((b & 0x80) != 0) { + b = input.ReadByte(); + result |= (b & 0x7F) << 28; + } + } + } + } + return optimizePositive ? result : ((result >> 1) ^ -(result & 1)); + } + + private string ReadString (Stream input) { + int charCount = ReadInt(input, true); + switch (charCount) { + case 0: + return null; + case 1: + return ""; + } + charCount--; + char[] chars = this.chars; + if (chars.Length < charCount) this.chars = chars = new char[charCount]; + // Try to read 7 bit ASCII chars. + int charIndex = 0; + int b = 0; + while (charIndex < charCount) { + b = input.ReadByte(); + if (b > 127) break; + chars[charIndex++] = (char)b; + } + // If a char was not ASCII, finish with slow path. + if (charIndex < charCount) ReadUtf8_slow(input, charCount, charIndex, b); + return new String(chars, 0, charCount); + } + + private void ReadUtf8_slow (Stream input, int charCount, int charIndex, int b) { + char[] chars = this.chars; + while (true) { + switch (b >> 4) { + case 0: + case 1: + case 2: + case 3: + case 4: + case 5: + case 6: + case 7: + chars[charIndex] = (char)b; + break; + case 12: + case 13: + chars[charIndex] = (char)((b & 0x1F) << 6 | input.ReadByte() & 0x3F); + break; + case 14: + chars[charIndex] = (char)((b & 0x0F) << 12 | (input.ReadByte() & 0x3F) << 6 | input.ReadByte() & 0x3F); + break; + } + if (++charIndex >= charCount) break; + b = input.ReadByte() & 0xFF; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/SkeletonBounds.cs b/SpineRuntimes/SpineRuntime21/SkeletonBounds.cs new file mode 100644 index 0000000..9593f0f --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/SkeletonBounds.cs @@ -0,0 +1,215 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class SkeletonBounds { + private List polygonPool = new List(); + private float minX, minY, maxX, maxY; + + public List BoundingBoxes { get; private set; } + public List Polygons { get; private set; } + public float MinX { get { return minX; } set { minX = value; } } + public float MinY { get { return minY; } set { minY = value; } } + public float MaxX { get { return maxX; } set { maxX = value; } } + public float MaxY { get { return maxY; } set { maxY = value; } } + public float Width { get { return maxX - minX; } } + public float Height { get { return maxY - minY; } } + + public SkeletonBounds () { + BoundingBoxes = new List(); + Polygons = new List(); + } + + public void Update (Skeleton skeleton, bool updateAabb) { + List boundingBoxes = BoundingBoxes; + List polygons = Polygons; + List slots = skeleton.slots; + int slotCount = slots.Count; + + boundingBoxes.Clear(); + foreach (Polygon polygon in polygons) + polygonPool.Add(polygon); + polygons.Clear(); + + for (int i = 0; i < slotCount; i++) { + Slot slot = slots[i]; + BoundingBoxAttachment boundingBox = slot.attachment as BoundingBoxAttachment; + if (boundingBox == null) continue; + boundingBoxes.Add(boundingBox); + + Polygon polygon = null; + int poolCount = polygonPool.Count; + if (poolCount > 0) { + polygon = polygonPool[poolCount - 1]; + polygonPool.RemoveAt(poolCount - 1); + } else + polygon = new Polygon(); + polygons.Add(polygon); + + int count = boundingBox.Vertices.Length; + polygon.Count = count; + if (polygon.Vertices.Length < count) polygon.Vertices = new float[count]; + boundingBox.ComputeWorldVertices(slot.bone, polygon.Vertices); + } + + if (updateAabb) aabbCompute(); + } + + private void aabbCompute () { + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + List polygons = Polygons; + for (int i = 0, n = polygons.Count; i < n; i++) { + Polygon polygon = polygons[i]; + float[] vertices = polygon.Vertices; + for (int ii = 0, nn = polygon.Count; ii < nn; ii += 2) { + float x = vertices[ii]; + float y = vertices[ii + 1]; + minX = Math.Min(minX, x); + minY = Math.Min(minY, y); + maxX = Math.Max(maxX, x); + maxY = Math.Max(maxY, y); + } + } + this.minX = minX; + this.minY = minY; + this.maxX = maxX; + this.maxY = maxY; + } + + + /// Returns true if the axis aligned bounding box contains the point. + public bool AabbContainsPoint (float x, float y) { + return x >= minX && x <= maxX && y >= minY && y <= maxY; + } + + /// Returns true if the axis aligned bounding box intersects the line segment. + public bool AabbIntersectsSegment (float x1, float y1, float x2, float y2) { + float minX = this.minX; + float minY = this.minY; + float maxX = this.maxX; + float maxY = this.maxY; + if ((x1 <= minX && x2 <= minX) || (y1 <= minY && y2 <= minY) || (x1 >= maxX && x2 >= maxX) || (y1 >= maxY && y2 >= maxY)) + return false; + float m = (y2 - y1) / (x2 - x1); + float y = m * (minX - x1) + y1; + if (y > minY && y < maxY) return true; + y = m * (maxX - x1) + y1; + if (y > minY && y < maxY) return true; + float x = (minY - y1) / m + x1; + if (x > minX && x < maxX) return true; + x = (maxY - y1) / m + x1; + if (x > minX && x < maxX) return true; + return false; + } + + /// Returns true if the axis aligned bounding box intersects the axis aligned bounding box of the specified bounds. + public bool AabbIntersectsSkeleton (SkeletonBounds bounds) { + return minX < bounds.maxX && maxX > bounds.minX && minY < bounds.maxY && maxY > bounds.minY; + } + + /// Returns true if the polygon contains the point. + public bool ContainsPoint (Polygon polygon, float x, float y) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + int prevIndex = nn - 2; + bool inside = false; + for (int ii = 0; ii < nn; ii += 2) { + float vertexY = vertices[ii + 1]; + float prevY = vertices[prevIndex + 1]; + if ((vertexY < y && prevY >= y) || (prevY < y && vertexY >= y)) { + float vertexX = vertices[ii]; + if (vertexX + (y - vertexY) / (prevY - vertexY) * (vertices[prevIndex] - vertexX) < x) inside = !inside; + } + prevIndex = ii; + } + return inside; + } + + /// Returns the first bounding box attachment that contains the point, or null. When doing many checks, it is usually more + /// efficient to only call this method if {@link #aabbContainsPoint(float, float)} returns true. + public BoundingBoxAttachment ContainsPoint (float x, float y) { + List polygons = Polygons; + for (int i = 0, n = polygons.Count; i < n; i++) + if (ContainsPoint(polygons[i], x, y)) return BoundingBoxes[i]; + return null; + } + + /// Returns the first bounding box attachment that contains the line segment, or null. When doing many checks, it is usually + /// more efficient to only call this method if {@link #aabbIntersectsSegment(float, float, float, float)} returns true. + public BoundingBoxAttachment IntersectsSegment (float x1, float y1, float x2, float y2) { + List polygons = Polygons; + for (int i = 0, n = polygons.Count; i < n; i++) + if (IntersectsSegment(polygons[i], x1, y1, x2, y2)) return BoundingBoxes[i]; + return null; + } + + /// Returns true if the polygon contains the line segment. + public bool IntersectsSegment (Polygon polygon, float x1, float y1, float x2, float y2) { + float[] vertices = polygon.Vertices; + int nn = polygon.Count; + + float width12 = x1 - x2, height12 = y1 - y2; + float det1 = x1 * y2 - y1 * x2; + float x3 = vertices[nn - 2], y3 = vertices[nn - 1]; + for (int ii = 0; ii < nn; ii += 2) { + float x4 = vertices[ii], y4 = vertices[ii + 1]; + float det2 = x3 * y4 - y3 * x4; + float width34 = x3 - x4, height34 = y3 - y4; + float det3 = width12 * height34 - height12 * width34; + float x = (det1 * width34 - width12 * det2) / det3; + if (((x >= x3 && x <= x4) || (x >= x4 && x <= x3)) && ((x >= x1 && x <= x2) || (x >= x2 && x <= x1))) { + float y = (det1 * height34 - height12 * det2) / det3; + if (((y >= y3 && y <= y4) || (y >= y4 && y <= y3)) && ((y >= y1 && y <= y2) || (y >= y2 && y <= y1))) return true; + } + x3 = x4; + y3 = y4; + } + return false; + } + + public Polygon getPolygon (BoundingBoxAttachment attachment) { + int index = BoundingBoxes.IndexOf(attachment); + return index == -1 ? null : Polygons[index]; + } + } + + public class Polygon { + public float[] Vertices { get; set; } + public int Count { get; set; } + + public Polygon () { + Vertices = new float[16]; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/SkeletonData.cs b/SpineRuntimes/SpineRuntime21/SkeletonData.cs new file mode 100644 index 0000000..0ea6536 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/SkeletonData.cs @@ -0,0 +1,158 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + public class SkeletonData { + internal String name; + internal List bones = new List(); + internal List slots = new List(); + internal List skins = new List(); + internal Skin defaultSkin; + internal List events = new List(); + internal List animations = new List(); + internal List ikConstraints = new List(); + internal float width, height; + internal String version, hash, imagesPath; + + public String Name { get { return name; } set { name = value; } } + public List Bones { get { return bones; } } // Ordered parents first. + public List Slots { get { return slots; } } // Setup pose draw order. + public List Skins { get { return skins; } set { skins = value; } } + /// May be null. + public Skin DefaultSkin { get { return defaultSkin; } set { defaultSkin = value; } } + public List Events { get { return events; } set { events = value; } } + public List Animations { get { return animations; } set { animations = value; } } + public List IkConstraints { get { return ikConstraints; } set { ikConstraints = value; } } + public float Width { get { return width; } set { width = value; } } + public float Height { get { return height; } set { height = value; } } + /// The Spine version used to export this data. + public String Version { get { return version; } set { version = value; } } + public String Hash { get { return hash; } set { hash = value; } } + + // --- Bones. + + /// May be null. + public BoneData FindBone (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + List bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) { + BoneData bone = bones[i]; + if (bone.name == boneName) return bone; + } + return null; + } + + /// -1 if the bone was not found. + public int FindBoneIndex (String boneName) { + if (boneName == null) throw new ArgumentNullException("boneName cannot be null."); + List bones = this.bones; + for (int i = 0, n = bones.Count; i < n; i++) + if (bones[i].name == boneName) return i; + return -1; + } + + // --- Slots. + + /// May be null. + public SlotData FindSlot (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) { + SlotData slot = slots[i]; + if (slot.name == slotName) return slot; + } + return null; + } + + /// -1 if the bone was not found. + public int FindSlotIndex (String slotName) { + if (slotName == null) throw new ArgumentNullException("slotName cannot be null."); + List slots = this.slots; + for (int i = 0, n = slots.Count; i < n; i++) + if (slots[i].name == slotName) return i; + return -1; + } + + // --- Skins. + + /// May be null. + public Skin FindSkin (String skinName) { + if (skinName == null) throw new ArgumentNullException("skinName cannot be null."); + foreach (Skin skin in skins) + if (skin.name == skinName) return skin; + return null; + } + + // --- Events. + + /// May be null. + public EventData FindEvent (String eventDataName) { + if (eventDataName == null) throw new ArgumentNullException("eventDataName cannot be null."); + foreach (EventData eventData in events) + if (eventData.name == eventDataName) return eventData; + return null; + } + + // --- Animations. + + /// May be null. + public Animation FindAnimation (String animationName) { + if (animationName == null) throw new ArgumentNullException("animationName cannot be null."); + List animations = this.animations; + for (int i = 0, n = animations.Count; i < n; i++) { + Animation animation = animations[i]; + if (animation.name == animationName) return animation; + } + return null; + } + + // --- IK constraints. + + /// May be null. + public IkConstraintData FindIkConstraint (String ikConstraintName) { + if (ikConstraintName == null) throw new ArgumentNullException("ikConstraintName cannot be null."); + List ikConstraints = this.ikConstraints; + for (int i = 0, n = ikConstraints.Count; i < n; i++) { + IkConstraintData ikConstraint = ikConstraints[i]; + if (ikConstraint.name == ikConstraintName) return ikConstraint; + } + return null; + } + + // --- + + override public String ToString () { + return name ?? base.ToString(); + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/SkeletonJson.cs b/SpineRuntimes/SpineRuntime21/SkeletonJson.cs new file mode 100644 index 0000000..bc995bf --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/SkeletonJson.cs @@ -0,0 +1,641 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.IO; +using System.Collections.Generic; + +#if WINDOWS_STOREAPP +using System.Threading.Tasks; +using Windows.Storage; +#endif + +namespace SpineRuntime21 { + public class SkeletonJson { + private AttachmentLoader attachmentLoader; + public float Scale { get; set; } + + public SkeletonJson (params Atlas[] atlasArray) + : this(new AtlasAttachmentLoader(atlasArray)) { + } + + public SkeletonJson (AttachmentLoader attachmentLoader) { + if (attachmentLoader == null) throw new ArgumentNullException("attachmentLoader cannot be null."); + this.attachmentLoader = attachmentLoader; + Scale = 1; + } + +#if WINDOWS_STOREAPP + private async Task ReadFile(string path) { + var folder = Windows.ApplicationModel.Package.Current.InstalledLocation; + var file = await folder.GetFileAsync(path).AsTask().ConfigureAwait(false); + using (var reader = new StreamReader(await file.OpenStreamForReadAsync().ConfigureAwait(false))) { + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.Name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } + + public SkeletonData ReadSkeletonData (String path) { + return this.ReadFile(path).Result; + } +#else + public SkeletonData ReadSkeletonData (String path) { +#if WINDOWS_PHONE + Stream stream = Microsoft.Xna.Framework.TitleContainer.OpenStream(path); + using (StreamReader reader = new StreamReader(stream)) { +#else + using (StreamReader reader = new StreamReader(path)) { +#endif + SkeletonData skeletonData = ReadSkeletonData(reader); + skeletonData.name = Path.GetFileNameWithoutExtension(path); + return skeletonData; + } + } +#endif + + public SkeletonData ReadSkeletonData (TextReader reader) { + if (reader == null) throw new ArgumentNullException("reader cannot be null."); + + var skeletonData = new SkeletonData(); + + var root = Json.Deserialize(reader) as Dictionary; + if (root == null) throw new Exception("Invalid JSON."); + + // Skeleton. + if (root.ContainsKey("skeleton")) { + var skeletonMap = (Dictionary)root["skeleton"]; + skeletonData.hash = (String)skeletonMap["hash"]; + skeletonData.version = (String)skeletonMap["spine"]; + skeletonData.width = GetFloat(skeletonMap, "width", 0); + skeletonData.height = GetFloat(skeletonMap, "height", 0); + } + + // Bones. + foreach (Dictionary boneMap in (List)root["bones"]) { + BoneData parent = null; + if (boneMap.ContainsKey("parent")) { + parent = skeletonData.FindBone((String)boneMap["parent"]); + if (parent == null) + throw new Exception("Parent bone not found: " + boneMap["parent"]); + } + var boneData = new BoneData((String)boneMap["name"], parent); + boneData.length = GetFloat(boneMap, "length", 0) * Scale; + boneData.x = GetFloat(boneMap, "x", 0) * Scale; + boneData.y = GetFloat(boneMap, "y", 0) * Scale; + boneData.rotation = GetFloat(boneMap, "rotation", 0); + boneData.scaleX = GetFloat(boneMap, "scaleX", 1); + boneData.scaleY = GetFloat(boneMap, "scaleY", 1); + boneData.flipX = GetBoolean(boneMap, "flipX", false); + boneData.flipY = GetBoolean(boneMap, "flipY", false); + boneData.inheritScale = GetBoolean(boneMap, "inheritScale", true); + boneData.inheritRotation = GetBoolean(boneMap, "inheritRotation", true); + skeletonData.bones.Add(boneData); + } + + // IK constraints. + if (root.ContainsKey("ik")) { + foreach (Dictionary ikMap in (List)root["ik"]) { + IkConstraintData ikConstraintData = new IkConstraintData((String)ikMap["name"]); + + foreach (String boneName in (List)ikMap["bones"]) { + BoneData bone = skeletonData.FindBone(boneName); + if (bone == null) throw new Exception("IK bone not found: " + boneName); + ikConstraintData.bones.Add(bone); + } + + String targetName = (String)ikMap["target"]; + ikConstraintData.target = skeletonData.FindBone(targetName); + if (ikConstraintData.target == null) throw new Exception("Target bone not found: " + targetName); + + ikConstraintData.bendDirection = GetBoolean(ikMap, "bendPositive", true) ? 1 : -1; + ikConstraintData.mix = GetFloat(ikMap, "mix", 1); + + skeletonData.ikConstraints.Add(ikConstraintData); + } + } + + // Slots. + if (root.ContainsKey("slots")) { + foreach (Dictionary slotMap in (List)root["slots"]) { + var slotName = (String)slotMap["name"]; + var boneName = (String)slotMap["bone"]; + BoneData boneData = skeletonData.FindBone(boneName); + if (boneData == null) + throw new Exception("Slot bone not found: " + boneName); + var slotData = new SlotData(slotName, boneData); + + if (slotMap.ContainsKey("color")) { + var color = (String)slotMap["color"]; + slotData.r = ToColor(color, 0); + slotData.g = ToColor(color, 1); + slotData.b = ToColor(color, 2); + slotData.a = ToColor(color, 3); + } + + if (slotMap.ContainsKey("attachment")) + slotData.attachmentName = (String)slotMap["attachment"]; + + if (slotMap.ContainsKey("additive")) + slotData.additiveBlending = (bool)slotMap["additive"]; + + skeletonData.slots.Add(slotData); + } + } + + // Skins. + if (root.ContainsKey("skins")) { + foreach (KeyValuePair entry in (Dictionary)root["skins"]) { + var skin = new Skin(entry.Key); + foreach (KeyValuePair slotEntry in (Dictionary)entry.Value) { + int slotIndex = skeletonData.FindSlotIndex(slotEntry.Key); + foreach (KeyValuePair attachmentEntry in ((Dictionary)slotEntry.Value)) { + Attachment attachment = ReadAttachment(skin, attachmentEntry.Key, (Dictionary)attachmentEntry.Value); + if (attachment != null) skin.AddAttachment(slotIndex, attachmentEntry.Key, attachment); + } + } + skeletonData.skins.Add(skin); + if (skin.name == "default") + skeletonData.defaultSkin = skin; + } + } + + // Events. + if (root.ContainsKey("events")) { + foreach (KeyValuePair entry in (Dictionary)root["events"]) { + var entryMap = (Dictionary)entry.Value; + var eventData = new EventData(entry.Key); + eventData.Int = GetInt(entryMap, "int", 0); + eventData.Float = GetFloat(entryMap, "float", 0); + eventData.String = GetString(entryMap, "string", null); + skeletonData.events.Add(eventData); + } + } + + // Animations. + if (root.ContainsKey("animations")) { + foreach (KeyValuePair entry in (Dictionary)root["animations"]) + ReadAnimation(entry.Key, (Dictionary)entry.Value, skeletonData); + } + + skeletonData.bones.TrimExcess(); + skeletonData.slots.TrimExcess(); + skeletonData.skins.TrimExcess(); + skeletonData.events.TrimExcess(); + skeletonData.animations.TrimExcess(); + skeletonData.ikConstraints.TrimExcess(); + return skeletonData; + } + + private Attachment ReadAttachment (Skin skin, String name, Dictionary map) { + if (map.ContainsKey("name")) + name = (String)map["name"]; + + var type = AttachmentType.region; + if (map.ContainsKey("type")) + type = (AttachmentType)Enum.Parse(typeof(AttachmentType), (String)map["type"], false); + + String path = name; + if (map.ContainsKey("path")) + path = (String)map["path"]; + + switch (type) { + case AttachmentType.region: + RegionAttachment region = attachmentLoader.NewRegionAttachment(skin, name, path); + if (region == null) return null; + region.Path = path; + region.x = GetFloat(map, "x", 0) * Scale; + region.y = GetFloat(map, "y", 0) * Scale; + region.scaleX = GetFloat(map, "scaleX", 1); + region.scaleY = GetFloat(map, "scaleY", 1); + region.rotation = GetFloat(map, "rotation", 0); + region.width = GetFloat(map, "width", 32) * Scale; + region.height = GetFloat(map, "height", 32) * Scale; + region.UpdateOffset(); + + if (map.ContainsKey("color")) { + var color = (String)map["color"]; + region.r = ToColor(color, 0); + region.g = ToColor(color, 1); + region.b = ToColor(color, 2); + region.a = ToColor(color, 3); + } + + return region; + case AttachmentType.mesh: { + MeshAttachment mesh = attachmentLoader.NewMeshAttachment(skin, name, path); + if (mesh == null) return null; + + mesh.Path = path; + mesh.vertices = GetFloatArray(map, "vertices", Scale); + mesh.triangles = GetIntArray(map, "triangles"); + mesh.regionUVs = GetFloatArray(map, "uvs", 1); + mesh.UpdateUVs(); + + if (map.ContainsKey("color")) { + var color = (String)map["color"]; + mesh.r = ToColor(color, 0); + mesh.g = ToColor(color, 1); + mesh.b = ToColor(color, 2); + mesh.a = ToColor(color, 3); + } + + mesh.HullLength = GetInt(map, "hull", 0) * 2; + if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges"); + mesh.Width = GetInt(map, "width", 0) * Scale; + mesh.Height = GetInt(map, "height", 0) * Scale; + + return mesh; + } + case AttachmentType.skinnedmesh: { + SkinnedMeshAttachment mesh = attachmentLoader.NewSkinnedMeshAttachment(skin, name, path); + if (mesh == null) return null; + + mesh.Path = path; + float[] uvs = GetFloatArray(map, "uvs", 1); + float[] vertices = GetFloatArray(map, "vertices", 1); + var weights = new List(uvs.Length * 3 * 3); + var bones = new List(uvs.Length * 3); + float scale = Scale; + 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; ) { + bones.Add((int)vertices[i]); + weights.Add(vertices[i + 1] * scale); + weights.Add(vertices[i + 2] * scale); + weights.Add(vertices[i + 3]); + i += 4; + } + } + mesh.bones = bones.ToArray(); + mesh.weights = weights.ToArray(); + mesh.triangles = GetIntArray(map, "triangles"); + mesh.regionUVs = uvs; + mesh.UpdateUVs(); + + if (map.ContainsKey("color")) { + var color = (String)map["color"]; + mesh.r = ToColor(color, 0); + mesh.g = ToColor(color, 1); + mesh.b = ToColor(color, 2); + mesh.a = ToColor(color, 3); + } + + mesh.HullLength = GetInt(map, "hull", 0) * 2; + if (map.ContainsKey("edges")) mesh.Edges = GetIntArray(map, "edges"); + mesh.Width = GetInt(map, "width", 0) * Scale; + mesh.Height = GetInt(map, "height", 0) * Scale; + + return mesh; + } + case AttachmentType.boundingbox: + BoundingBoxAttachment box = attachmentLoader.NewBoundingBoxAttachment(skin, name); + if (box == null) return null; + box.vertices = GetFloatArray(map, "vertices", Scale); + return box; + } + return null; + } + + private float[] GetFloatArray (Dictionary map, String name, float scale) { + var list = (List)map[name]; + var values = new float[list.Count]; + if (scale == 1) { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i]; + } else { + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (float)list[i] * scale; + } + return values; + } + + private int[] GetIntArray (Dictionary map, String name) { + var list = (List)map[name]; + var values = new int[list.Count]; + for (int i = 0, n = list.Count; i < n; i++) + values[i] = (int)(float)list[i]; + return values; + } + + private float GetFloat (Dictionary map, String name, float defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (float)map[name]; + } + + private int GetInt (Dictionary map, String name, int defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (int)(float)map[name]; + } + + private bool GetBoolean (Dictionary map, String name, bool defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (bool)map[name]; + } + + private String GetString (Dictionary map, String name, String defaultValue) { + if (!map.ContainsKey(name)) + return defaultValue; + return (String)map[name]; + } + + private float ToColor (String hexString, int colorIndex) { + if (hexString.Length != 8) + throw new ArgumentException("Color hexidecimal length must be 8, recieved: " + hexString); + return Convert.ToInt32(hexString.Substring(colorIndex * 2, 2), 16) / (float)255; + } + + private void ReadAnimation (String name, Dictionary map, SkeletonData skeletonData) { + var timelines = new List(); + float duration = 0; + float scale = Scale; + + if (map.ContainsKey("slots")) { + foreach (KeyValuePair entry in (Dictionary)map["slots"]) { + String slotName = entry.Key; + int slotIndex = skeletonData.FindSlotIndex(slotName); + var timelineMap = (Dictionary)entry.Value; + + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var timelineName = (String)timelineEntry.Key; + if (timelineName == "color") { + var timeline = new ColorTimeline(values.Count); + timeline.slotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + String c = (String)valueMap["color"]; + timeline.SetFrame(frameIndex, time, ToColor(c, 0), ToColor(c, 1), ToColor(c, 2), ToColor(c, 3)); + ReadCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 5 - 5]); + + } else if (timelineName == "attachment") { + var timeline = new AttachmentTimeline(values.Count); + timeline.slotIndex = slotIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + timeline.SetFrame(frameIndex++, time, (String)valueMap["name"]); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + + } else + throw new Exception("Invalid timeline type for a slot: " + timelineName + " (" + slotName + ")"); + } + } + } + + if (map.ContainsKey("bones")) { + foreach (KeyValuePair entry in (Dictionary)map["bones"]) { + String boneName = entry.Key; + int boneIndex = skeletonData.FindBoneIndex(boneName); + if (boneIndex == -1) + throw new Exception("Bone not found: " + boneName); + + var timelineMap = (Dictionary)entry.Value; + foreach (KeyValuePair timelineEntry in timelineMap) { + var values = (List)timelineEntry.Value; + var timelineName = (String)timelineEntry.Key; + if (timelineName == "rotate") { + var timeline = new RotateTimeline(values.Count); + timeline.boneIndex = boneIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + timeline.SetFrame(frameIndex, time, (float)valueMap["angle"]); + ReadCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 2 - 2]); + + } else if (timelineName == "translate" || timelineName == "scale") { + TranslateTimeline timeline; + float timelineScale = 1; + if (timelineName == "scale") + timeline = new ScaleTimeline(values.Count); + else { + timeline = new TranslateTimeline(values.Count); + timelineScale = scale; + } + timeline.boneIndex = boneIndex; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + float x = valueMap.ContainsKey("x") ? (float)valueMap["x"] : 0; + float y = valueMap.ContainsKey("y") ? (float)valueMap["y"] : 0; + timeline.SetFrame(frameIndex, time, (float)x * timelineScale, (float)y * timelineScale); + ReadCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 3 - 3]); + + } else if (timelineName == "flipX" || timelineName == "flipY") { + bool x = timelineName == "flipX"; + var timeline = x ? new FlipXTimeline(values.Count) : new FlipYTimeline(values.Count); + timeline.boneIndex = boneIndex; + + String field = x ? "x" : "y"; + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + timeline.SetFrame(frameIndex, time, valueMap.ContainsKey(field) ? (bool)valueMap[field] : false); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 2 - 2]); + + } else + throw new Exception("Invalid timeline type for a bone: " + timelineName + " (" + boneName + ")"); + } + } + } + + if (map.ContainsKey("ik")) { + foreach (KeyValuePair ikMap in (Dictionary)map["ik"]) { + IkConstraintData ikConstraint = skeletonData.FindIkConstraint(ikMap.Key); + var values = (List)ikMap.Value; + var timeline = new IkConstraintTimeline(values.Count); + timeline.ikConstraintIndex = skeletonData.ikConstraints.IndexOf(ikConstraint); + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float time = (float)valueMap["time"]; + float mix = valueMap.ContainsKey("mix") ? (float)valueMap["mix"] : 1; + bool bendPositive = valueMap.ContainsKey("bendPositive") ? (bool)valueMap["bendPositive"] : true; + timeline.SetFrame(frameIndex, time, mix, bendPositive ? 1 : -1); + ReadCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount * 3 - 3]); + } + } + + if (map.ContainsKey("ffd")) { + foreach (KeyValuePair ffdMap in (Dictionary)map["ffd"]) { + Skin skin = skeletonData.FindSkin(ffdMap.Key); + foreach (KeyValuePair slotMap in (Dictionary)ffdMap.Value) { + int slotIndex = skeletonData.FindSlotIndex(slotMap.Key); + foreach (KeyValuePair meshMap in (Dictionary)slotMap.Value) { + var values = (List)meshMap.Value; + var timeline = new FFDTimeline(values.Count); + Attachment attachment = skin.GetAttachment(slotIndex, meshMap.Key); + if (attachment == null) throw new Exception("FFD attachment not found: " + meshMap.Key); + timeline.slotIndex = slotIndex; + timeline.attachment = attachment; + + int vertexCount; + if (attachment is MeshAttachment) + vertexCount = ((MeshAttachment)attachment).vertices.Length; + else + vertexCount = ((SkinnedMeshAttachment)attachment).Weights.Length / 3 * 2; + + int frameIndex = 0; + foreach (Dictionary valueMap in values) { + float[] vertices; + if (!valueMap.ContainsKey("vertices")) { + if (attachment is MeshAttachment) + vertices = ((MeshAttachment)attachment).vertices; + else + vertices = new float[vertexCount]; + } else { + var verticesValue = (List)valueMap["vertices"]; + vertices = new float[vertexCount]; + int start = GetInt(valueMap, "offset", 0); + if (scale == 1) { + for (int i = 0, n = verticesValue.Count; i < n; i++) + vertices[i + start] = (float)verticesValue[i]; + } else { + for (int i = 0, n = verticesValue.Count; i < n; i++) + vertices[i + start] = (float)verticesValue[i] * scale; + } + if (attachment is MeshAttachment) { + float[] meshVertices = ((MeshAttachment)attachment).vertices; + for (int i = 0; i < vertexCount; i++) + vertices[i] += meshVertices[i]; + } + } + + timeline.SetFrame(frameIndex, (float)valueMap["time"], vertices); + ReadCurve(timeline, frameIndex, valueMap); + frameIndex++; + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + } + } + } + } + + if (map.ContainsKey("drawOrder") || map.ContainsKey("draworder")) { + var values = (List)map[map.ContainsKey("drawOrder") ? "drawOrder" : "draworder"]; + var timeline = new DrawOrderTimeline(values.Count); + int slotCount = skeletonData.slots.Count; + int frameIndex = 0; + foreach (Dictionary drawOrderMap in values) { + int[] drawOrder = null; + if (drawOrderMap.ContainsKey("offsets")) { + drawOrder = new int[slotCount]; + for (int i = slotCount - 1; i >= 0; i--) + drawOrder[i] = -1; + var offsets = (List)drawOrderMap["offsets"]; + int[] unchanged = new int[slotCount - offsets.Count]; + int originalIndex = 0, unchangedIndex = 0; + foreach (Dictionary offsetMap in offsets) { + int slotIndex = skeletonData.FindSlotIndex((String)offsetMap["slot"]); + if (slotIndex == -1) throw new Exception("Slot not found: " + offsetMap["slot"]); + // Collect unchanged items. + while (originalIndex != slotIndex) + unchanged[unchangedIndex++] = originalIndex++; + // Set changed items. + int index = originalIndex + (int)(float)offsetMap["offset"]; + drawOrder[index] = 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++, (float)drawOrderMap["time"], drawOrder); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + } + + if (map.ContainsKey("events")) { + var eventsMap = (List)map["events"]; + var timeline = new EventTimeline(eventsMap.Count); + int frameIndex = 0; + foreach (Dictionary eventMap in eventsMap) { + EventData eventData = skeletonData.FindEvent((String)eventMap["name"]); + if (eventData == null) throw new Exception("Event not found: " + eventMap["name"]); + var e = new Event(eventData); + e.Int = GetInt(eventMap, "int", eventData.Int); + e.Float = GetFloat(eventMap, "float", eventData.Float); + e.String = GetString(eventMap, "string", eventData.String); + timeline.SetFrame(frameIndex++, (float)eventMap["time"], e); + } + timelines.Add(timeline); + duration = Math.Max(duration, timeline.frames[timeline.FrameCount - 1]); + } + + timelines.TrimExcess(); + skeletonData.animations.Add(new Animation(name, timelines, duration)); + } + + private void ReadCurve (CurveTimeline timeline, int frameIndex, Dictionary valueMap) { + if (!valueMap.ContainsKey("curve")) + return; + Object curveObject = valueMap["curve"]; + if (curveObject.Equals("stepped")) + timeline.SetStepped(frameIndex); + else if (curveObject is List) { + var curve = (List)curveObject; + timeline.SetCurve(frameIndex, (float)curve[0], (float)curve[1], (float)curve[2], (float)curve[3]); + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Skin.cs b/SpineRuntimes/SpineRuntime21/Skin.cs new file mode 100644 index 0000000..247f28f --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Skin.cs @@ -0,0 +1,101 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; +using System.Collections.Generic; + +namespace SpineRuntime21 { + /// Stores attachments by slot index and attachment name. + public class Skin { + internal String name; + private Dictionary, Attachment> attachments = + new Dictionary, Attachment>(AttachmentComparer.Instance); + + public String Name { get { return name; } } + + public Skin (String name) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + this.name = name; + } + + public void AddAttachment (int slotIndex, String name, Attachment attachment) { + if (attachment == null) throw new ArgumentNullException("attachment cannot be null."); + attachments[new KeyValuePair(slotIndex, name)] = attachment; + } + + /// May be null. + public Attachment GetAttachment (int slotIndex, String name) { + Attachment attachment; + attachments.TryGetValue(new KeyValuePair(slotIndex, name), out attachment); + return attachment; + } + + public void FindNamesForSlot (int slotIndex, List names) { + if (names == null) throw new ArgumentNullException("names cannot be null."); + foreach (KeyValuePair key in attachments.Keys) + if (key.Key == slotIndex) names.Add(key.Value); + } + + public void FindAttachmentsForSlot (int slotIndex, List attachments) { + if (attachments == null) throw new ArgumentNullException("attachments cannot be null."); + foreach (KeyValuePair, Attachment> entry in this.attachments) + if (entry.Key.Key == slotIndex) attachments.Add(entry.Value); + } + + override public String ToString () { + return name; + } + + /// Attach all attachments from this skin if the corresponding attachment from the old skin is currently attached. + internal void AttachAll (Skeleton skeleton, Skin oldSkin) { + foreach (KeyValuePair, Attachment> entry in oldSkin.attachments) { + int slotIndex = entry.Key.Key; + Slot slot = skeleton.slots[slotIndex]; + if (slot.attachment == entry.Value) { + Attachment attachment = GetAttachment(slotIndex, entry.Key.Value); + if (attachment != null) slot.Attachment = attachment; + } + } + } + + // Avoids boxing in the dictionary. + private class AttachmentComparer : IEqualityComparer> { + internal static readonly AttachmentComparer Instance = new AttachmentComparer(); + + bool IEqualityComparer>.Equals (KeyValuePair o1, KeyValuePair o2) { + return o1.Key == o2.Key && o1.Value == o2.Value; + } + + int IEqualityComparer>.GetHashCode (KeyValuePair o) { + return o.Key; + } + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/Slot.cs b/SpineRuntimes/SpineRuntime21/Slot.cs new file mode 100644 index 0000000..865da4d --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/Slot.cs @@ -0,0 +1,99 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public class Slot { + internal SlotData data; + internal Bone bone; + internal float r, g, b, a; + internal Attachment attachment; + internal float attachmentTime; + internal float[] attachmentVertices = new float[0]; + internal int attachmentVerticesCount; + + public SlotData Data { get { return data; } } + public Bone Bone { get { return bone; } } + public Skeleton Skeleton { get { return bone.skeleton; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + + /// May be null. + public Attachment Attachment { + get { + return attachment; + } + set { + attachment = value; + attachmentTime = bone.skeleton.time; + attachmentVerticesCount = 0; + } + } + + public float AttachmentTime { + get { + return bone.skeleton.time - attachmentTime; + } + set { + attachmentTime = bone.skeleton.time - value; + } + } + + public float[] AttachmentVertices { get { return attachmentVertices; } set { attachmentVertices = value; } } + public int AttachmentVerticesCount { get { return attachmentVerticesCount; } set { attachmentVerticesCount = value; } } + + public Slot (SlotData data, Bone bone) { + if (data == null) throw new ArgumentNullException("data cannot be null."); + if (bone == null) throw new ArgumentNullException("bone cannot be null."); + this.data = data; + this.bone = bone; + SetToSetupPose(); + } + + internal void SetToSetupPose (int slotIndex) { + r = data.r; + g = data.g; + b = data.b; + a = data.a; + Attachment = data.attachmentName == null ? null : bone.skeleton.GetAttachment(slotIndex, data.attachmentName); + } + + public void SetToSetupPose () { + SetToSetupPose(bone.skeleton.data.slots.IndexOf(data)); + } + + override public String ToString () { + return data.name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/SlotData.cs b/SpineRuntimes/SpineRuntime21/SlotData.cs new file mode 100644 index 0000000..5edfb34 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/SlotData.cs @@ -0,0 +1,62 @@ +/****************************************************************************** + * Spine Runtimes Software License + * Version 2.1 + * + * Copyright (c) 2013, Esoteric Software + * All rights reserved. + * + * You are granted a perpetual, non-exclusive, non-sublicensable and + * non-transferable license to install, execute and perform the Spine Runtimes + * Software (the "Software") solely for internal use. Without the written + * permission of Esoteric Software (typically granted by licensing Spine), you + * may not (a) modify, translate, adapt or otherwise create derivative works, + * improvements of the Software or develop new applications using the Software + * 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 SOFTARE BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; + * OR BUSINESS INTERRUPTION) 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. + *****************************************************************************/ + +using System; + +namespace SpineRuntime21 { + public class SlotData { + internal String name; + internal BoneData boneData; + internal float r = 1, g = 1, b = 1, a = 1; + internal String attachmentName; + internal bool additiveBlending; + + public String Name { get { return name; } } + public BoneData BoneData { get { return boneData; } } + public float R { get { return r; } set { r = value; } } + public float G { get { return g; } set { g = value; } } + public float B { get { return b; } set { b = value; } } + public float A { get { return a; } set { a = value; } } + /// May be null. + public String AttachmentName { get { return attachmentName; } set { attachmentName = value; } } + public bool AdditiveBlending { get { return additiveBlending; } set { additiveBlending = value; } } + + public SlotData (String name, BoneData boneData) { + if (name == null) throw new ArgumentNullException("name cannot be null."); + if (boneData == null) throw new ArgumentNullException("boneData cannot be null."); + this.name = name; + this.boneData = boneData; + } + + override public String ToString () { + return name; + } + } +} diff --git a/SpineRuntimes/SpineRuntime21/SpineRuntime21.csproj b/SpineRuntimes/SpineRuntime21/SpineRuntime21.csproj new file mode 100644 index 0000000..dd26fa6 --- /dev/null +++ b/SpineRuntimes/SpineRuntime21/SpineRuntime21.csproj @@ -0,0 +1,14 @@ + + + + enable + enable + x64 + net8.0-windows + win-x64 + $(SolutionDir)out + false + 2.1.25 + + + diff --git a/SpineViewer.sln b/SpineViewer.sln index 9335978..9c78181 100644 --- a/SpineViewer.sln +++ b/SpineViewer.sln @@ -26,6 +26,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime41", "SpineRunt EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime42", "SpineRuntimes\SpineRuntime42\SpineRuntime42.csproj", "{1D96AAF6-AB7B-8050-4C7E-03431778628F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SpineRuntime21", "SpineRuntimes\SpineRuntime21\SpineRuntime21.csproj", "{628CA98E-1D21-2282-C01E-0470CAF211E1}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|x64 = Debug|x64 @@ -60,6 +62,10 @@ Global {1D96AAF6-AB7B-8050-4C7E-03431778628F}.Debug|x64.Build.0 = Debug|x64 {1D96AAF6-AB7B-8050-4C7E-03431778628F}.Release|x64.ActiveCfg = Release|x64 {1D96AAF6-AB7B-8050-4C7E-03431778628F}.Release|x64.Build.0 = Release|x64 + {628CA98E-1D21-2282-C01E-0470CAF211E1}.Debug|x64.ActiveCfg = Debug|x64 + {628CA98E-1D21-2282-C01E-0470CAF211E1}.Debug|x64.Build.0 = Debug|x64 + {628CA98E-1D21-2282-C01E-0470CAF211E1}.Release|x64.ActiveCfg = Release|x64 + {628CA98E-1D21-2282-C01E-0470CAF211E1}.Release|x64.Build.0 = Release|x64 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -71,6 +77,7 @@ Global {2E19353C-9C0B-85F7-4EF4-98A778A79059} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} {C7B93D57-A896-38B2-1D43-25B28502F756} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} {1D96AAF6-AB7B-8050-4C7E-03431778628F} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} + {628CA98E-1D21-2282-C01E-0470CAF211E1} = {EA2E1399-02BC-43BC-AD9F-42E23E9C3DA8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {91F0EFD1-4B07-4C3C-82D8-90432349D3A5} diff --git a/SpineViewer/Spine/Implementations/Spine21.cs b/SpineViewer/Spine/Implementations/Spine21.cs new file mode 100644 index 0000000..352fb49 --- /dev/null +++ b/SpineViewer/Spine/Implementations/Spine21.cs @@ -0,0 +1,374 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using SpineRuntime21; + +namespace SpineViewer.Spine.Implementations +{ + [SpineImplementation(Version.V21)] + internal class Spine21 : Spine + { + private class TextureLoader : SpineRuntime21.TextureLoader + { + public void Load(AtlasPage page, string path) + { + var texture = new SFML.Graphics.Texture(path); + if (page.magFilter == TextureFilter.Linear) + texture.Smooth = true; + if (page.uWrap == TextureWrap.Repeat && page.vWrap == TextureWrap.Repeat) + texture.Repeated = true; + + page.rendererObject = texture; + page.width = (int)texture.Size.X; + page.height = (int)texture.Size.Y; + } + + public void Unload(object texture) + { + ((SFML.Graphics.Texture)texture).Dispose(); + } + } + private static TextureLoader textureLoader = new(); + + private Atlas atlas; + private SkeletonBinary? skeletonBinary; + private SkeletonJson? skeletonJson; + private SkeletonData skeletonData; + private AnimationStateData animationStateData; + + private Skeleton skeleton; + private AnimationState animationState; + + // 2.1.x 不支持剪裁 + //private SkeletonClipping clipping = new(); + + public Spine21(string skelPath, string? atlasPath = null) : base(skelPath, atlasPath) + { + atlas = new Atlas(AtlasPath, textureLoader); + try + { + // 先尝试二进制文件 + skeletonJson = null; + skeletonBinary = new SkeletonBinary(atlas); + skeletonData = skeletonBinary.ReadSkeletonData(SkelPath); + } + catch + { + try + { + // 再尝试 Json 文件 + skeletonBinary = null; + skeletonJson = new SkeletonJson(atlas); + skeletonData = skeletonJson.ReadSkeletonData(SkelPath); + } + catch + { + // 都不行就报错 + throw new ArgumentException($"Unknown skeleton file format {SkelPath}"); + } + } + + animationStateData = new AnimationStateData(skeletonData); + skeleton = new Skeleton(skeletonData); + animationState = new AnimationState(animationStateData); + + foreach (var anime in skeletonData.Animations) + animationNames.Add(anime.Name); + CurrentAnimation = DefaultAnimationName; + } + + protected override void Dispose(bool disposing) + { + base.Dispose(disposing); + atlas.Dispose(); + } + + public override float Scale + { + get + { + if (skeletonBinary is not null) + return skeletonBinary.Scale; + else if (skeletonJson is not null) + return skeletonJson.Scale; + else + return 1f; + } + set + { + // 保存状态 + var position = Position; + var flipX = FlipX; + var flipY = FlipY; + var savedTrack0 = animationState.GetCurrent(0); + + var val = Math.Max(value, SCALE_MIN); + if (skeletonBinary is not null) + { + skeletonBinary.Scale = val; + skeletonData = skeletonBinary.ReadSkeletonData(SkelPath); + } + else if (skeletonJson is not null) + { + skeletonJson.Scale = val; + skeletonData = skeletonJson.ReadSkeletonData(SkelPath); + } + + // reload skel-dependent data + animationStateData = new AnimationStateData(skeletonData) { DefaultMix = animationStateData.DefaultMix }; + skeleton = new Skeleton(skeletonData); + animationState = new AnimationState(animationStateData); + + // 恢复状态 + Position = position; + FlipX = flipX; + FlipY = flipY; + + // 恢复原本 Track0 上所有动画 + if (savedTrack0 is not null) + { + var entry = animationState.SetAnimation(0, savedTrack0.Animation.Name, true); + entry.Time = savedTrack0.Time; + // 2.1.x 没有提供 Next 访问器,故放弃还原后续动画,问题不大,因为预览画面目前不需要连续播放不同动画,只需要循环同一个动画 + //var savedEntry = savedTrack0.Next; + //while (savedEntry is not null) + //{ + // entry = animationState.AddAnimation(0, savedEntry.Animation.Name, true, 0); + // entry.Time = savedEntry.TrackTime; + // savedEntry = savedEntry.Next; + //} + } + } + } + + public override PointF Position + { + get => new(skeleton.X, skeleton.Y); + set + { + skeleton.X = value.X; + skeleton.Y = value.Y; + } + } + + public override bool FlipX + { + get => skeleton.FlipX; + set => skeleton.FlipX = value; + } + + public override bool FlipY + { + get => skeleton.FlipY; + set => skeleton.FlipY = value; + } + + public override string CurrentAnimation + { + get => animationState.GetCurrent(0)?.Animation.Name ?? DefaultAnimationName; + set { if (animationNames.Contains(value)) { animationState.SetAnimation(0, value, true); Update(0); } } + } + + public override RectangleF Bounds + { + get + { + float[] temp = new float[8]; + var drawOrderItems = skeleton.DrawOrder; + float minX = int.MaxValue, minY = int.MaxValue, maxX = int.MinValue, maxY = int.MinValue; + for (int i = 0, n = skeleton.DrawOrder.Count; i < n; i++) + { + Slot slot = drawOrderItems[i]; + int verticesLength = 0; + float[] vertices = null; + Attachment attachment = slot.Attachment; + var regionAttachment = attachment as RegionAttachment; + if (regionAttachment != null) + { + verticesLength = 8; + vertices = temp; + if (vertices.Length < 8) vertices = temp = new float[8]; + regionAttachment.ComputeWorldVertices(slot.Bone, temp); + } + else + { + var meshAttachment = attachment as MeshAttachment; + if (meshAttachment != null) + { + MeshAttachment mesh = meshAttachment; + verticesLength = mesh.Vertices.Length; + vertices = temp; + if (vertices.Length < verticesLength) vertices = temp = new float[verticesLength]; + mesh.ComputeWorldVertices(slot, temp); + } + } + + if (vertices != null) + { + for (int ii = 0; ii < verticesLength; ii += 2) + { + float vx = vertices[ii], vy = vertices[ii + 1]; + minX = Math.Min(minX, vx); + minY = Math.Min(minY, vy); + maxX = Math.Max(maxX, vx); + maxY = Math.Max(maxY, vy); + } + } + } + return new RectangleF(minX, minY, maxX - minX, maxY - minY); + } + } + + public override float GetAnimationDuration(string name) { return skeletonData.FindAnimation(name)?.Duration ?? 0f; } + + public override void Update(float delta) + { + skeleton.Update(delta); + animationState.Update(delta); + animationState.Apply(skeleton); + skeleton.UpdateWorldTransform(); + } + + //private SFML.Graphics.BlendMode GetSFMLBlendMode(SpineRuntime21.BlendMode spineBlendMode) + //{ + // return spineBlendMode switch + // { + // SpineRuntime21.BlendMode.Normal => BlendMode.Normal, + // SpineRuntime21.BlendMode.Additive => BlendMode.Additive, + // SpineRuntime21.BlendMode.Multiply => BlendMode.Multiply, + // SpineRuntime21.BlendMode.Screen => BlendMode.Screen, + // _ => throw new NotImplementedException($"{spineBlendMode}"), + // }; + //} + + public override void Draw(SFML.Graphics.RenderTarget target, SFML.Graphics.RenderStates states) + { + vertexArray.Clear(); + states.Texture = null; + + // 要用 DrawOrder 而不是 Slots + foreach (var slot in skeleton.DrawOrder) + { + var attachment = slot.Attachment; + + SFML.Graphics.Texture texture; + + float[] worldVertices = worldVerticesBuffer; // 顶点世界坐标, 连续的 [x0, y0, x1, y1, ...] 坐标值 + int worldVerticesCount; // 等于顶点数组的长度除以 2 + int[] worldTriangleIndices; // 三角形索引, 从顶点坐标数组取的时候要乘以 2, 最大值是 worldVerticesCount - 1 + int worldTriangleIndicesLength; // 三角形索引数组长度 + float[] uvs; // 纹理坐标 + float tintR = skeleton.R * slot.R; + float tintG = skeleton.G * slot.G; + float tintB = skeleton.B * slot.B; + float tintA = skeleton.A * slot.A; + + if (attachment is RegionAttachment regionAttachment) + { + texture = (SFML.Graphics.Texture)((AtlasRegion)regionAttachment.RendererObject).page.rendererObject; + + regionAttachment.ComputeWorldVertices(slot.Bone, worldVertices); + worldVerticesCount = 4; + worldTriangleIndices = [0, 1, 2, 2, 3, 0]; + worldTriangleIndicesLength = 6; + uvs = regionAttachment.UVs; + tintR *= regionAttachment.R; + tintG *= regionAttachment.G; + tintB *= regionAttachment.B; + tintA *= regionAttachment.A; + } + else if (attachment is MeshAttachment meshAttachment) + { + texture = (SFML.Graphics.Texture)((AtlasRegion)meshAttachment.RendererObject).page.rendererObject; + + if (meshAttachment.Vertices.Length > worldVertices.Length) + worldVertices = worldVerticesBuffer = new float[meshAttachment.Vertices.Length * 2]; + meshAttachment.ComputeWorldVertices(slot, worldVertices); + worldVerticesCount = meshAttachment.Vertices.Length / 2; + worldTriangleIndices = meshAttachment.Triangles; + worldTriangleIndicesLength = meshAttachment.Triangles.Length; + uvs = meshAttachment.UVs; + tintR *= meshAttachment.R; + tintG *= meshAttachment.G; + tintB *= meshAttachment.B; + tintA *= meshAttachment.A; + } + // 2.1.x 不支持剪裁 + //else if (attachment is ClippingAttachment clippingAttachment) + //{ + // clipping.ClipStart(slot, clippingAttachment); + // continue; + //} + else + { + //clipping.ClipEnd(slot); + continue; + } + + // 似乎 2.1.x 也没有 BlendMode + SFML.Graphics.BlendMode blendMode = slot.Data.AdditiveBlending ? BlendMode.Additive : BlendMode.Normal; + + states.Texture ??= texture; + if (states.BlendMode != blendMode || states.Texture != texture) + { + if (vertexArray.VertexCount > 0) + { + if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive)) + states.Shader = FragmentShader; + else + states.Shader = null; + target.Draw(vertexArray, states); + vertexArray.Clear(); + } + states.BlendMode = blendMode; + states.Texture = texture; + } + + //if (clipping.IsClipping) + //{ + // // 这里必须单独记录 Count, 和 Items 的 Length 是不一致的 + // clipping.ClipTriangles(worldVertices, worldVerticesCount * 2, worldTriangleIndices, worldTriangleIndicesLength, uvs); + // worldVertices = clipping.ClippedVertices.Items; + // worldVerticesCount = clipping.ClippedVertices.Count / 2; + // worldTriangleIndices = clipping.ClippedTriangles.Items; + // worldTriangleIndicesLength = clipping.ClippedTriangles.Count; + // uvs = clipping.ClippedUVs.Items; + //} + + var textureSizeX = texture.Size.X; + var textureSizeY = texture.Size.Y; + + SFML.Graphics.Vertex vertex = new(); + vertex.Color.R = (byte)(tintR * 255); + vertex.Color.G = (byte)(tintG * 255); + vertex.Color.B = (byte)(tintB * 255); + vertex.Color.A = (byte)(tintA * 255); + + // 必须用 worldTriangleIndicesLength 不能直接 foreach + for (int i = 0; i < worldTriangleIndicesLength; i++) + { + var index = worldTriangleIndices[i] * 2; + vertex.Position.X = worldVertices[index]; + vertex.Position.Y = worldVertices[index + 1]; + vertex.TexCoords.X = uvs[index] * textureSizeX; + vertex.TexCoords.Y = uvs[index + 1] * textureSizeY; + vertexArray.Append(vertex); + } + + //clipping.ClipEnd(slot); + } + + if (UsePremultipliedAlpha && (states.BlendMode == BlendMode.Normal || states.BlendMode == BlendMode.Additive)) + states.Shader = FragmentShader; + else + states.Shader = null; + target.Draw(vertexArray, states); + //clipping.ClipEnd(); + } + } +} diff --git a/SpineViewer/Spine/Version.cs b/SpineViewer/Spine/Version.cs index df2884c..352467b 100644 --- a/SpineViewer/Spine/Version.cs +++ b/SpineViewer/Spine/Version.cs @@ -40,6 +40,7 @@ namespace SpineViewer.Spine /// public enum Version { + [Description("v2.1.x")] V21 = 0x0201, [Description("v3.6.x")] V36 = 0x0306, [Description("v3.7.x")] V37 = 0x0307, [Description("v3.8.x")] V38 = 0x0308, diff --git a/SpineViewer/SpineViewer.csproj b/SpineViewer/SpineViewer.csproj index f9e0766..1ee5f89 100644 --- a/SpineViewer/SpineViewer.csproj +++ b/SpineViewer/SpineViewer.csproj @@ -25,6 +25,7 @@ +