/****************************************************************************** * Spine Runtimes License Agreement * Last updated July 28, 2023. Replaces all prior versions. * * Copyright (c) 2013-2023, Esoteric Software LLC * * Integration of the Spine Runtimes into software or otherwise creating * derivative works of the Spine Runtimes is permitted under the terms and * conditions of Section 2 of the Spine Editor License Agreement: * http://esotericsoftware.com/spine-editor-license * * Otherwise, it is permitted to integrate the Spine Runtimes into software or * otherwise create derivative works of the Spine Runtimes (collectively, * "Products"), provided that each user of the Products must obtain their own * Spine Editor license and redistribution of the Products in any form must * include this license and copyright notice. * * THE SPINE RUNTIMES ARE PROVIDED BY ESOTERIC SOFTWARE LLC "AS IS" AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL ESOTERIC SOFTWARE LLC BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES, * BUSINESS INTERRUPTION, OR LOSS OF USE, DATA, OR PROFITS) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THE * SPINE RUNTIMES, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. *****************************************************************************/ using System; using System.Collections.Generic; namespace Spine { /// /// Stores a list of timelines to animate a skeleton's pose over time. public class Animation { internal String name; internal ExposedList timelines; internal HashSet timelineIds; internal float duration; public Animation (string name, ExposedList timelines, float duration) { if (name == null) throw new ArgumentNullException("name", "name cannot be null."); this.name = name; SetTimelines(timelines); this.duration = duration; } public ExposedList Timelines { get { return timelines; } set { SetTimelines(value); } } public void SetTimelines (ExposedList timelines) { if (timelines == null) throw new ArgumentNullException("timelines", "timelines cannot be null."); this.timelines = timelines; // Note: avoiding reallocations by adding all hash set entries at // once (EnsureCapacity() is only available in newer .Net versions). int idCount = 0; int timelinesCount = timelines.Count; Timeline[] timelinesItems = timelines.Items; for (int t = 0; t < timelinesCount; ++t) idCount += timelinesItems[t].PropertyIds.Length; string[] propertyIds = new string[idCount]; int currentId = 0; for (int t = 0; t < timelinesCount; ++t) { string[] ids = timelinesItems[t].PropertyIds; for (int i = 0, idsLength = ids.Length; i < idsLength; ++i) propertyIds[currentId++] = ids[i]; } this.timelineIds = new HashSet(propertyIds); } /// The duration of the animation in seconds, which is usually the highest time of all frames in the timeline. The duration is /// used to know when it has completed and when it should loop back to the start. public float Duration { get { return duration; } set { duration = value; } } /// The animation's name, which is unique across all animations in the skeleton. public string Name { get { return name; } } /// Returns true if this animation contains a timeline with any of the specified property IDs. public bool HasTimeline (string[] propertyIds) { foreach (string id in propertyIds) if (timelineIds.Contains(id)) return true; return false; } /// Applies the animation's timelines to the specified skeleton. /// /// The skeleton the animation is being applied to. This provides access to the bones, slots, and other skeleton /// components the timelines may change. /// The last time in seconds this animation was applied. Some timelines trigger only at specific times rather /// than every frame. Pass -1 the first time an animation is applied to ensure frame 0 is triggered. /// The time in seconds the skeleton is being posed for. Most timelines find the frame before and the frame after /// this time and interpolate between the frame values. If beyond the and loop is /// true then the animation will repeat, else the last frame will be applied. /// If true, the animation repeats after the . /// If any events are fired, they are added to this list. Can be null to ignore fired events or if no timelines /// fire events. /// 0 applies the current or setup values (depending on blend). 1 applies the timeline values. Between /// 0 and 1 applies values between the current or setup values and the timeline values. By adjusting /// alpha over time, an animation can be mixed in or out. alpha can also be useful to apply /// animations on top of each other (layering). /// Controls how mixing is applied when alpha < 1. /// Indicates whether the timelines are mixing in or out. Used by timelines which perform instant transitions, /// such as or . public void Apply (Skeleton skeleton, float lastTime, float time, bool loop, ExposedList events, float alpha, MixBlend blend, MixDirection direction) { if (skeleton == null) throw new ArgumentNullException("skeleton", "skeleton cannot be null."); if (loop && duration != 0) { time %= duration; if (lastTime > 0) lastTime %= duration; } Timeline[] timelines = this.timelines.Items; for (int i = 0, n = this.timelines.Count; i < n; i++) timelines[i].Apply(skeleton, lastTime, time, events, alpha, blend, direction); } override public string ToString () { return name; } } /// /// Controls how timeline values are mixed with setup pose values or current pose values when a timeline is applied with /// alpha < 1. /// public enum MixBlend { /// Transitions from the setup value to the timeline value (the current value is not used). Before the first frame, the /// setup value is set. Setup, /// /// /// Transitions from the current value to the timeline value. Before the first frame, transitions from the current value to /// the setup value. Timelines which perform instant transitions, such as or /// , use the setup value before the first frame. /// /// First is intended for the first animations applied, not for animations layered on top of those. /// First, /// /// /// Transitions from the current value to the timeline value. No change is made before the first frame (the current value is /// kept until the first frame). /// /// Replace is intended for animations layered on top of others, not for the first animations applied. /// Replace, /// /// /// Transitions from the current value to the current value plus the timeline value. No change is made before the first frame /// (the current value is kept until the first frame). /// /// Add is intended for animations layered on top of others, not for the first animations applied. Properties /// set by additive animations must be set manually or by another animation before applying the additive animations, else the /// property values will increase each time the additive animations are applied. /// /// Add } /// /// Indicates whether a timeline's alpha is mixing out over time toward 0 (the setup or current pose value) or /// mixing in toward 1 (the timeline's value). Some timelines use this to decide how values are applied. /// public enum MixDirection { In, Out } public enum Property { Rotate = 0, X, Y, ScaleX, ScaleY, ShearX, ShearY, Inherit, // RGB, Alpha, RGB2, // Attachment, Deform, // Event, DrawOrder, // IkConstraint, TransformConstraint, // PathConstraintPosition, PathConstraintSpacing, PathConstraintMix, // PhysicsConstraintInertia, PhysicsConstraintStrength, PhysicsConstraintDamping, PhysicsConstraintMass, // PhysicsConstraintWind, PhysicsConstraintGravity, PhysicsConstraintMix, PhysicsConstraintReset, // Sequence } /// /// The base class for all timelines. public abstract class Timeline { private readonly string[] propertyIds; internal readonly float[] frames; /// Unique identifiers for the properties the timeline modifies. public Timeline (int frameCount, params string[] propertyIds) { if (propertyIds == null) throw new System.ArgumentNullException("propertyIds", "propertyIds cannot be null."); this.propertyIds = propertyIds; frames = new float[frameCount * FrameEntries]; } /// Uniquely encodes both the type of this timeline and the skeleton properties that it affects. public string[] PropertyIds { get { return propertyIds; } } /// The time in seconds and any other values for each frame. public float[] Frames { get { return frames; } } /// The number of entries stored per frame. public virtual int FrameEntries { get { return 1; } } /// The number of frames for this timeline. public virtual int FrameCount { get { return frames.Length / FrameEntries; } } public float Duration { get { return frames[frames.Length - FrameEntries]; } } /// Applies this timeline to the skeleton. /// The skeleton the timeline is being applied to. This provides access to the bones, slots, and other /// skeleton components the timeline may change. /// The time this timeline was last applied. Timelines such as trigger only /// at specific times rather than every frame. In that case, the timeline triggers everything between /// lastTime (exclusive) and time (inclusive). Pass -1 the first time an animation is /// applied to ensure frame 0 is triggered. /// The time in seconds that the skeleton is being posed for. Most timelines find the frame before and the frame /// after this time and interpolate between the frame values.If beyond the last frame, the last frame will be /// applied. /// If any events are fired, they are added to this list. Can be null to ignore fired events or if the timeline /// does not fire events. /// 0 applies the current or setup value (depending on blend). 1 applies the timeline value. /// Between 0 and 1 applies a value between the current or setup value and the timeline value.By adjusting /// alpha over time, an animation can be mixed in or out. alpha can also be useful to /// apply animations on top of each other (layering). /// Controls how mixing is applied when alpha < 1. /// Indicates whether the timeline is mixing in or out. Used by timelines which perform instant transitions, /// such as or , and other such as . public abstract void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, MixDirection direction); /// Search using a stride of 1. /// Must be >= the first value in frames. /// The index of the first value <= time. internal static int Search (float[] frames, float time) { int n = frames.Length; for (int i = 1; i < n; i++) if (frames[i] > time) return i - 1; return n - 1; } /// Search using the specified stride. /// Must be >= the first value in frames. /// The index of the first value <= time. internal static int Search (float[] frames, float time, int step) { int n = frames.Length; for (int i = step; i < n; i += step) if (frames[i] > time) return i - step; return n - step; } } /// An interface for timelines which change the property of a bone. public interface IBoneTimeline { /// The index of the bone in that will be changed when this timeline is applied. int BoneIndex { get; } } /// An interface for timelines which change the property of a slot. public interface ISlotTimeline { /// The index of the slot in that will be changed when this timeline is applied. int SlotIndex { get; } } /// The base class for timelines that interpolate between frame values using stepped, linear, or a Bezier curve. public abstract class CurveTimeline : Timeline { public const int LINEAR = 0, STEPPED = 1, BEZIER = 2, BEZIER_SIZE = 18; internal float[] curves; /// The number of key frames for this timeline. /// The maximum number of Bezier curves. See . /// Unique identifiers for the properties the timeline modifies. public CurveTimeline (int frameCount, int bezierCount, params string[] propertyIds) : base(frameCount, propertyIds) { curves = new float[frameCount + bezierCount * BEZIER_SIZE]; curves[frameCount - 1] = STEPPED; } /// Sets the specified frame to linear interpolation. /// Between 0 and frameCount - 1, inclusive. public void SetLinear (int frame) { curves[frame] = LINEAR; } /// Sets the specified frame to stepped interpolation. /// Between 0 and frameCount - 1, inclusive. public void SetStepped (int frame) { curves[frame] = STEPPED; } /// Returns the interpolation type for the specified frame. /// Between 0 and frameCount - 1, inclusive. /// , or + the index of the Bezier segments. public float GetCurveType (int frame) { return (int)curves[frame]; } /// Shrinks the storage for Bezier curves, for use when bezierCount (specified in the constructor) was larger /// than the actual number of Bezier curves. public void Shrink (int bezierCount) { int size = FrameCount + bezierCount * BEZIER_SIZE; if (curves.Length > size) { float[] newCurves = new float[size]; Array.Copy(curves, 0, newCurves, 0, size); curves = newCurves; } } /// /// Stores the segments for the specified Bezier curve. For timelines that modify multiple values, there may be more than /// one curve per frame. /// The ordinal of this Bezier curve for this timeline, between 0 and bezierCount - 1 (specified /// in the constructor), inclusive. /// Between 0 and frameCount - 1, inclusive. /// The index of the value for the frame this curve is used for. /// The time for the first key. /// The value for the first key. /// The time for the first Bezier handle. /// The value for the first Bezier handle. /// The time of the second Bezier handle. /// The value for the second Bezier handle. /// The time for the second key. /// The value for the second key. public void SetBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2, float cy2, float time2, float value2) { float[] curves = this.curves; int i = FrameCount + bezier * BEZIER_SIZE; if (value == 0) curves[frame] = BEZIER + i; float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = (value1 - cy1 * 2 + cy2) * 0.03f; float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = ((cy1 - cy2) * 3 - value1 + value2) * 0.006f; float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy; float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = (cy1 - value1) * 0.3f + tmpy + dddy * 0.16666667f; float x = time1 + dx, y = value1 + dy; for (int n = i + BEZIER_SIZE; i < n; i += 2) { curves[i] = x; curves[i + 1] = y; dx += ddx; dy += ddy; ddx += dddx; ddy += dddy; x += dx; y += dy; } } /// /// Returns the Bezier interpolated value for the specified time. /// The index into for the values of the frame before time. /// The offset from frameIndex to the value this curve is used for. /// The index of the Bezier segments. See . public float GetBezierValue (float time, int frameIndex, int valueOffset, int i) { float[] curves = this.curves; if (curves[i] > time) { float x = frames[frameIndex], y = frames[frameIndex + valueOffset]; return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); } int n = i + BEZIER_SIZE; for (i += 2; i < n; i += 2) { if (curves[i] >= time) { float x = curves[i - 2], y = curves[i - 1]; return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); } } frameIndex += FrameEntries; { // scope added to prevent compile error "float x and y declared in enclosing scope" float x = curves[n - 2], y = curves[n - 1]; return y + (time - x) / (frames[frameIndex] - x) * (frames[frameIndex + valueOffset] - y); } } } /// The base class for a that sets one property. public abstract class CurveTimeline1 : CurveTimeline { public const int ENTRIES = 2; internal const int VALUE = 1; /// The maximum number of Bezier curves. See . /// Unique identifiers for the properties the timeline modifies. public CurveTimeline1 (int frameCount, int bezierCount, string propertyId) : base(frameCount, bezierCount, propertyId) { } public override int FrameEntries { get { return ENTRIES; } } /// Sets the time and value for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds public void SetFrame (int frame, float time, float value) { frame <<= 1; frames[frame] = time; frames[frame + VALUE] = value; } /// Returns the interpolated value for the specified time. public float GetCurveValue (float time) { float[] frames = this.frames; int i = frames.Length - 2; for (int ii = 2; ii <= i; ii += 2) { if (frames[ii] > time) { i = ii - 2; break; } } int curveType = (int)curves[i >> 1]; switch (curveType) { case LINEAR: float before = frames[i], value = frames[i + VALUE]; return value + (time - before) / (frames[i + ENTRIES] - before) * (frames[i + ENTRIES + VALUE] - value); case STEPPED: return frames[i + VALUE]; } return GetBezierValue(time, i, VALUE, curveType - BEZIER); } public float GetRelativeValue (float time, float alpha, MixBlend blend, float current, float setup) { if (time < frames[0]) { switch (blend) { case MixBlend.Setup: return setup; case MixBlend.First: return current + (setup - current) * alpha; } return current; } float value = GetCurveValue(time); switch (blend) { case MixBlend.Setup: return setup + value * alpha; case MixBlend.First: case MixBlend.Replace: value += setup - current; break; } return current + value * alpha; } public float GetAbsoluteValue (float time, float alpha, MixBlend blend, float current, float setup) { if (time < frames[0]) { switch (blend) { case MixBlend.Setup: return setup; case MixBlend.First: return current + (setup - current) * alpha; } return current; } float value = GetCurveValue(time); if (blend == MixBlend.Setup) return setup + (value - setup) * alpha; return current + (value - current) * alpha; } public float GetAbsoluteValue (float time, float alpha, MixBlend blend, float current, float setup, float value) { if (time < frames[0]) { switch (blend) { case MixBlend.Setup: return setup; case MixBlend.First: return current + (setup - current) * alpha; } return current; } if (blend == MixBlend.Setup) return setup + (value - setup) * alpha; return current + (value - current) * alpha; } public float GetScaleValue (float time, float alpha, MixBlend blend, MixDirection direction, float current, float setup) { float[] frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.Setup: return setup; case MixBlend.First: return current + (setup - current) * alpha; } return current; } float value = GetCurveValue(time) * setup; if (alpha == 1) { if (blend == MixBlend.Add) return current + value - setup; return value; } // Mixing out uses sign of setup or current pose, else use sign of key. if (direction == MixDirection.Out) { switch (blend) { case MixBlend.Setup: return setup + (Math.Abs(value) * Math.Sign(setup) - setup) * alpha; case MixBlend.First: case MixBlend.Replace: return current + (Math.Abs(value) * Math.Sign(current) - current) * alpha; } } else { float s; switch (blend) { case MixBlend.Setup: s = Math.Abs(setup) * Math.Sign(value); return s + (value - s) * alpha; case MixBlend.First: case MixBlend.Replace: s = Math.Abs(current) * Math.Sign(value); return s + (value - s) * alpha; } } return current + (value - setup) * alpha; } } /// The base class for a which sets two properties. public abstract class CurveTimeline2 : CurveTimeline { public const int ENTRIES = 3; internal const int VALUE1 = 1, VALUE2 = 2; /// The maximum number of Bezier curves. See . /// Unique identifiers for the properties the timeline modifies. public CurveTimeline2 (int frameCount, int bezierCount, string propertyId1, string propertyId2) : base(frameCount, bezierCount, propertyId1, propertyId2) { } public override int FrameEntries { get { return ENTRIES; } } /// Sets the time and values for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float value1, float value2) { frame *= ENTRIES; frames[frame] = time; frames[frame + VALUE1] = value1; frames[frame + VALUE2] = value2; } } /// Changes a bone's local . public class RotateTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public RotateTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.Rotate + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.rotation = GetRelativeValue(time, alpha, blend, bone.rotation, bone.data.rotation); } } /// Changes a bone's local and . public class TranslateTimeline : CurveTimeline2, IBoneTimeline { readonly int boneIndex; public TranslateTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, // (int)Property.X + "|" + boneIndex, // (int)Property.Y + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (!bone.active) return; float[] frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.Setup: bone.x = bone.data.x; bone.y = bone.data.y; return; case MixBlend.First: bone.x += (bone.data.x - bone.x) * alpha; bone.y += (bone.data.y - bone.y) * alpha; return; } return; } float x, y; GetCurveValue(out x, out y, time); // note: reference implementation has code inlined switch (blend) { case MixBlend.Setup: bone.x = bone.data.x + x * alpha; bone.y = bone.data.y + y * alpha; break; case MixBlend.First: case MixBlend.Replace: bone.x += (bone.data.x + x - bone.x) * alpha; bone.y += (bone.data.y + y - bone.y) * alpha; break; case MixBlend.Add: bone.x += x * alpha; bone.y += y * alpha; break; } } public void GetCurveValue (out float x, out float y, float time) { int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; x = frames[i + VALUE1]; y = frames[i + VALUE2]; float t = (time - before) / (frames[i + ENTRIES] - before); x += (frames[i + ENTRIES + VALUE1] - x) * t; y += (frames[i + ENTRIES + VALUE2] - y) * t; break; case STEPPED: x = frames[i + VALUE1]; y = frames[i + VALUE2]; break; default: x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); break; } } } /// Changes a bone's local . public class TranslateXTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public TranslateXTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.X + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.x = GetRelativeValue(time, alpha, blend, bone.x, bone.data.x); } } /// Changes a bone's local . public class TranslateYTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public TranslateYTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.Y + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.y = GetRelativeValue(time, alpha, blend, bone.y, bone.data.y); } } /// Changes a bone's local and . public class ScaleTimeline : CurveTimeline2, IBoneTimeline { readonly int boneIndex; public ScaleTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, // (int)Property.ScaleX + "|" + boneIndex, // (int)Property.ScaleY + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (!bone.active) return; float[] frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.Setup: bone.scaleX = bone.data.scaleX; bone.scaleY = bone.data.scaleY; return; case MixBlend.First: bone.scaleX += (bone.data.scaleX - bone.scaleX) * alpha; bone.scaleY += (bone.data.scaleY - bone.scaleY) * alpha; return; } return; } float x, y; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; x = frames[i + VALUE1]; y = frames[i + VALUE2]; float t = (time - before) / (frames[i + ENTRIES] - before); x += (frames[i + ENTRIES + VALUE1] - x) * t; y += (frames[i + ENTRIES + VALUE2] - y) * t; break; case STEPPED: x = frames[i + VALUE1]; y = frames[i + VALUE2]; break; default: x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); break; } x *= bone.data.scaleX; y *= bone.data.scaleY; if (alpha == 1) { if (blend == MixBlend.Add) { bone.scaleX += x - bone.data.scaleX; bone.scaleY += y - bone.data.scaleY; } else { bone.scaleX = x; bone.scaleY = y; } } else { // Mixing out uses sign of setup or current pose, else use sign of key. float bx, by; if (direction == MixDirection.Out) { switch (blend) { case MixBlend.Setup: bx = bone.data.scaleX; by = bone.data.scaleY; bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; break; case MixBlend.First: case MixBlend.Replace: bx = bone.scaleX; by = bone.scaleY; bone.scaleX = bx + (Math.Abs(x) * Math.Sign(bx) - bx) * alpha; bone.scaleY = by + (Math.Abs(y) * Math.Sign(by) - by) * alpha; break; case MixBlend.Add: bone.scaleX += (x - bone.data.scaleX) * alpha; bone.scaleY += (y - bone.data.scaleY) * alpha; break; } } else { switch (blend) { case MixBlend.Setup: bx = Math.Abs(bone.data.scaleX) * Math.Sign(x); by = Math.Abs(bone.data.scaleY) * Math.Sign(y); bone.scaleX = bx + (x - bx) * alpha; bone.scaleY = by + (y - by) * alpha; break; case MixBlend.First: case MixBlend.Replace: bx = Math.Abs(bone.scaleX) * Math.Sign(x); by = Math.Abs(bone.scaleY) * Math.Sign(y); bone.scaleX = bx + (x - bx) * alpha; bone.scaleY = by + (y - by) * alpha; break; case MixBlend.Add: bone.scaleX += (x - bone.data.scaleX) * alpha; bone.scaleY += (y - bone.data.scaleY) * alpha; break; } } } } } /// Changes a bone's local . public class ScaleXTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public ScaleXTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.ScaleX + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.scaleX = GetScaleValue(time, alpha, blend, direction, bone.scaleX, bone.data.scaleX); } } /// Changes a bone's local . public class ScaleYTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public ScaleYTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.ScaleY + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.scaleY = GetScaleValue(time, alpha, blend, direction, bone.scaleY, bone.data.scaleY); } } /// Changes a bone's local and . public class ShearTimeline : CurveTimeline2, IBoneTimeline { readonly int boneIndex; public ShearTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, // (int)Property.ShearX + "|" + boneIndex, // (int)Property.ShearY + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (!bone.active) return; float[] frames = this.frames; if (time < frames[0]) { // Time is before first frame. switch (blend) { case MixBlend.Setup: bone.shearX = bone.data.shearX; bone.shearY = bone.data.shearY; return; case MixBlend.First: bone.shearX += (bone.data.shearX - bone.shearX) * alpha; bone.shearY += (bone.data.shearY - bone.shearY) * alpha; return; } return; } float x, y; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; x = frames[i + VALUE1]; y = frames[i + VALUE2]; float t = (time - before) / (frames[i + ENTRIES] - before); x += (frames[i + ENTRIES + VALUE1] - x) * t; y += (frames[i + ENTRIES + VALUE2] - y) * t; break; case STEPPED: x = frames[i + VALUE1]; y = frames[i + VALUE2]; break; default: x = GetBezierValue(time, i, VALUE1, curveType - BEZIER); y = GetBezierValue(time, i, VALUE2, curveType + BEZIER_SIZE - BEZIER); break; } switch (blend) { case MixBlend.Setup: bone.shearX = bone.data.shearX + x * alpha; bone.shearY = bone.data.shearY + y * alpha; break; case MixBlend.First: case MixBlend.Replace: bone.shearX += (bone.data.shearX + x - bone.shearX) * alpha; bone.shearY += (bone.data.shearY + y - bone.shearY) * alpha; break; case MixBlend.Add: bone.shearX += x * alpha; bone.shearY += y * alpha; break; } } } /// Changes a bone's local . public class ShearXTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public ShearXTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.ShearX + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.shearX = GetRelativeValue(time, alpha, blend, bone.shearX, bone.data.shearX); } } /// Changes a bone's local . public class ShearYTimeline : CurveTimeline1, IBoneTimeline { readonly int boneIndex; public ShearYTimeline (int frameCount, int bezierCount, int boneIndex) : base(frameCount, bezierCount, (int)Property.ShearY + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (bone.active) bone.shearY = GetRelativeValue(time, alpha, blend, bone.shearY, bone.data.shearY); } } /// Changes a bone's . public class InheritTimeline : Timeline, IBoneTimeline { public const int ENTRIES = 2; public const int INHERIT = 1; readonly int boneIndex; public InheritTimeline (int frameCount, int boneIndex) : base(frameCount, (int)Property.Inherit + "|" + boneIndex) { this.boneIndex = boneIndex; } public int BoneIndex { get { return boneIndex; } } public override int FrameEntries { get { return ENTRIES; } } /// Sets the transform mode for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, Inherit inherit) { frame *= ENTRIES; frames[frame] = time; frames[frame + INHERIT] = (int)inherit; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Bone bone = skeleton.bones.Items[boneIndex]; if (!bone.active) return; if (direction == MixDirection.Out) { if (blend == MixBlend.Setup) bone.inherit = bone.data.inherit; return; } float[] frames = this.frames; if (time < frames[0]) { if (blend == MixBlend.Setup || blend == MixBlend.First) bone.inherit = bone.data.inherit; return; } bone.inherit = InheritEnum.Values[(int)frames[Search(frames, time, ENTRIES) + INHERIT]]; } } /// Changes a slot's . public class RGBATimeline : CurveTimeline, ISlotTimeline { public const int ENTRIES = 5; protected const int R = 1, G = 2, B = 3, A = 4; readonly int slotIndex; public RGBATimeline (int frameCount, int bezierCount, int slotIndex) : base(frameCount, bezierCount, // (int)Property.RGB + "|" + slotIndex, // (int)Property.Alpha + "|" + slotIndex) { this.slotIndex = slotIndex; } public override int FrameEntries { get { return ENTRIES; } } public int SlotIndex { get { return slotIndex; } } /// Sets the time and color for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float r, float g, float b, float a) { frame *= ENTRIES; frames[frame] = time; frames[frame + R] = r; frames[frame + G] = g; frames[frame + B] = b; frames[frame + A] = a; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; float[] frames = this.frames; if (time < frames[0]) { SlotData setup = slot.data; switch (blend) { case MixBlend.Setup: slot.r = setup.r; slot.g = setup.g; slot.b = setup.b; slot.a = setup.a; return; case MixBlend.First: slot.r += (setup.r - slot.r) * alpha; slot.g += (setup.g - slot.g) * alpha; slot.b += (setup.b - slot.b) * alpha; slot.a += (setup.a - slot.a) * alpha; slot.ClampColor(); return; } return; } float r, g, b, a; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; a = frames[i + A]; float t = (time - before) / (frames[i + ENTRIES] - before); r += (frames[i + ENTRIES + R] - r) * t; g += (frames[i + ENTRIES + G] - g) * t; b += (frames[i + ENTRIES + B] - b) * t; a += (frames[i + ENTRIES + A] - a) * t; break; case STEPPED: r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; a = frames[i + A]; break; default: r = GetBezierValue(time, i, R, curveType - BEZIER); g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); a = GetBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER); break; } if (alpha == 1) { slot.r = r; slot.g = g; slot.b = b; slot.a = a; } else { float br, bg, bb, ba; if (blend == MixBlend.Setup) { br = slot.data.r; bg = slot.data.g; bb = slot.data.b; ba = slot.data.a; } else { br = slot.r; bg = slot.g; bb = slot.b; ba = slot.a; } slot.r = br + (r - br) * alpha; slot.g = bg + (g - bg) * alpha; slot.b = bb + (b - bb) * alpha; slot.a = ba + (a - ba) * alpha; } slot.ClampColor(); } } /// Changes the RGB for a slot's . public class RGBTimeline : CurveTimeline, ISlotTimeline { public const int ENTRIES = 4; protected const int R = 1, G = 2, B = 3; readonly int slotIndex; public RGBTimeline (int frameCount, int bezierCount, int slotIndex) : base(frameCount, bezierCount, // (int)Property.RGB + "|" + slotIndex) { this.slotIndex = slotIndex; } public override int FrameEntries { get { return ENTRIES; } } public int SlotIndex { get { return slotIndex; } } /// Sets the time and color for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float r, float g, float b) { frame <<= 2; frames[frame] = time; frames[frame + R] = r; frames[frame + G] = g; frames[frame + B] = b; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; float[] frames = this.frames; if (time < frames[0]) { SlotData setup = slot.data; switch (blend) { case MixBlend.Setup: slot.r = setup.r; slot.g = setup.g; slot.b = setup.b; return; case MixBlend.First: slot.r += (setup.r - slot.r) * alpha; slot.g += (setup.g - slot.g) * alpha; slot.b += (setup.b - slot.b) * alpha; slot.ClampColor(); return; } return; } float r, g, b; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 2]; switch (curveType) { case LINEAR: float before = frames[i]; r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; float t = (time - before) / (frames[i + ENTRIES] - before); r += (frames[i + ENTRIES + R] - r) * t; g += (frames[i + ENTRIES + G] - g) * t; b += (frames[i + ENTRIES + B] - b) * t; break; case STEPPED: r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; break; default: r = GetBezierValue(time, i, R, curveType - BEZIER); g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); break; } if (alpha == 1) { slot.r = r; slot.g = g; slot.b = b; } else { float br, bg, bb; if (blend == MixBlend.Setup) { SlotData setup = slot.data; br = setup.r; bg = setup.g; bb = setup.b; } else { br = slot.r; bg = slot.g; bb = slot.b; } slot.r = br + (r - br) * alpha; slot.g = bg + (g - bg) * alpha; slot.b = bb + (b - bb) * alpha; } slot.ClampColor(); } } /// Changes the alpha for a slot's . public class AlphaTimeline : CurveTimeline1, ISlotTimeline { readonly int slotIndex; public AlphaTimeline (int frameCount, int bezierCount, int slotIndex) : base(frameCount, bezierCount, (int)Property.Alpha + "|" + slotIndex) { this.slotIndex = slotIndex; } public int SlotIndex { get { return slotIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; float[] frames = this.frames; if (time < frames[0]) { SlotData setup = slot.data; switch (blend) { case MixBlend.Setup: slot.a = setup.a; return; case MixBlend.First: slot.a += (setup.a - slot.a) * alpha; slot.ClampColor(); return; } return; } float a = GetCurveValue(time); if (alpha == 1) slot.a = a; else { if (blend == MixBlend.Setup) slot.a = slot.data.a; slot.a += (a - slot.a) * alpha; } slot.ClampColor(); } } /// Changes a slot's and for two color tinting. public class RGBA2Timeline : CurveTimeline, ISlotTimeline { public const int ENTRIES = 8; protected const int R = 1, G = 2, B = 3, A = 4, R2 = 5, G2 = 6, B2 = 7; readonly int slotIndex; public RGBA2Timeline (int frameCount, int bezierCount, int slotIndex) : base(frameCount, bezierCount, // (int)Property.RGB + "|" + slotIndex, // (int)Property.Alpha + "|" + slotIndex, // (int)Property.RGB2 + "|" + slotIndex) { this.slotIndex = slotIndex; } public override int FrameEntries { get { return ENTRIES; } } /// /// The index of the slot in that will be changed when this timeline is applied. The /// must have a dark color available. public int SlotIndex { get { return slotIndex; } } /// Sets the time, light color, and dark color for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float r, float g, float b, float a, float r2, float g2, float b2) { frame <<= 3; frames[frame] = time; frames[frame + R] = r; frames[frame + G] = g; frames[frame + B] = b; frames[frame + A] = a; frames[frame + R2] = r2; frames[frame + G2] = g2; frames[frame + B2] = b2; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; float[] frames = this.frames; if (time < frames[0]) { SlotData setup = slot.data; switch (blend) { case MixBlend.Setup: slot.r = setup.r; slot.g = setup.g; slot.b = setup.b; slot.a = setup.a; slot.ClampColor(); slot.r2 = setup.r2; slot.g2 = setup.g2; slot.b2 = setup.b2; slot.ClampSecondColor(); return; case MixBlend.First: slot.r += (slot.r - setup.r) * alpha; slot.g += (slot.g - setup.g) * alpha; slot.b += (slot.b - setup.b) * alpha; slot.a += (slot.a - setup.a) * alpha; slot.ClampColor(); slot.r2 += (slot.r2 - setup.r2) * alpha; slot.g2 += (slot.g2 - setup.g2) * alpha; slot.b2 += (slot.b2 - setup.b2) * alpha; slot.ClampSecondColor(); return; } return; } float r, g, b, a, r2, g2, b2; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 3]; switch (curveType) { case LINEAR: float before = frames[i]; r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; a = frames[i + A]; r2 = frames[i + R2]; g2 = frames[i + G2]; b2 = frames[i + B2]; float t = (time - before) / (frames[i + ENTRIES] - before); r += (frames[i + ENTRIES + R] - r) * t; g += (frames[i + ENTRIES + G] - g) * t; b += (frames[i + ENTRIES + B] - b) * t; a += (frames[i + ENTRIES + A] - a) * t; r2 += (frames[i + ENTRIES + R2] - r2) * t; g2 += (frames[i + ENTRIES + G2] - g2) * t; b2 += (frames[i + ENTRIES + B2] - b2) * t; break; case STEPPED: r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; a = frames[i + A]; r2 = frames[i + R2]; g2 = frames[i + G2]; b2 = frames[i + B2]; break; default: r = GetBezierValue(time, i, R, curveType - BEZIER); g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); a = GetBezierValue(time, i, A, curveType + BEZIER_SIZE * 3 - BEZIER); r2 = GetBezierValue(time, i, R2, curveType + BEZIER_SIZE * 4 - BEZIER); g2 = GetBezierValue(time, i, G2, curveType + BEZIER_SIZE * 5 - BEZIER); b2 = GetBezierValue(time, i, B2, curveType + BEZIER_SIZE * 6 - BEZIER); break; } if (alpha == 1) { slot.r = r; slot.g = g; slot.b = b; slot.a = a; slot.r2 = r2; slot.g2 = g2; slot.b2 = b2; } else { float br, bg, bb, ba, br2, bg2, bb2; if (blend == MixBlend.Setup) { br = slot.data.r; bg = slot.data.g; bb = slot.data.b; ba = slot.data.a; br2 = slot.data.r2; bg2 = slot.data.g2; bb2 = slot.data.b2; } else { br = slot.r; bg = slot.g; bb = slot.b; ba = slot.a; br2 = slot.r2; bg2 = slot.g2; bb2 = slot.b2; } slot.r = br + (r - br) * alpha; slot.g = bg + (g - bg) * alpha; slot.b = bb + (b - bb) * alpha; slot.a = ba + (a - ba) * alpha; slot.r2 = br2 + (r2 - br2) * alpha; slot.g2 = bg2 + (g2 - bg2) * alpha; slot.b2 = bb2 + (b2 - bb2) * alpha; } slot.ClampColor(); slot.ClampSecondColor(); } } /// Changes the RGB for a slot's and for two color tinting. public class RGB2Timeline : CurveTimeline, ISlotTimeline { public const int ENTRIES = 7; protected const int R = 1, G = 2, B = 3, R2 = 4, G2 = 5, B2 = 6; readonly int slotIndex; public RGB2Timeline (int frameCount, int bezierCount, int slotIndex) : base(frameCount, bezierCount, // (int)Property.RGB + "|" + slotIndex, // (int)Property.RGB2 + "|" + slotIndex) { this.slotIndex = slotIndex; } public override int FrameEntries { get { return ENTRIES; } } /// /// The index of the slot in that will be changed when this timeline is applied. The /// must have a dark color available. public int SlotIndex { get { return slotIndex; } } /// Sets the time, light color, and dark color for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float r, float g, float b, float r2, float g2, float b2) { frame *= ENTRIES; frames[frame] = time; frames[frame + R] = r; frames[frame + G] = g; frames[frame + B] = b; frames[frame + R2] = r2; frames[frame + G2] = g2; frames[frame + B2] = b2; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; float[] frames = this.frames; if (time < frames[0]) { SlotData setup = slot.data; switch (blend) { case MixBlend.Setup: slot.r = setup.r; slot.g = setup.g; slot.b = setup.b; slot.ClampColor(); slot.r2 = setup.r2; slot.g2 = setup.g2; slot.b2 = setup.b2; slot.ClampSecondColor(); return; case MixBlend.First: slot.r += (slot.r - setup.r) * alpha; slot.g += (slot.g - setup.g) * alpha; slot.b += (slot.b - setup.b) * alpha; slot.ClampColor(); slot.r2 += (slot.r2 - setup.r2) * alpha; slot.g2 += (slot.g2 - setup.g2) * alpha; slot.b2 += (slot.b2 - setup.b2) * alpha; slot.ClampSecondColor(); return; } return; } float r, g, b, r2, g2, b2; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; r2 = frames[i + R2]; g2 = frames[i + G2]; b2 = frames[i + B2]; float t = (time - before) / (frames[i + ENTRIES] - before); r += (frames[i + ENTRIES + R] - r) * t; g += (frames[i + ENTRIES + G] - g) * t; b += (frames[i + ENTRIES + B] - b) * t; r2 += (frames[i + ENTRIES + R2] - r2) * t; g2 += (frames[i + ENTRIES + G2] - g2) * t; b2 += (frames[i + ENTRIES + B2] - b2) * t; break; case STEPPED: r = frames[i + R]; g = frames[i + G]; b = frames[i + B]; r2 = frames[i + R2]; g2 = frames[i + G2]; b2 = frames[i + B2]; break; default: r = GetBezierValue(time, i, R, curveType - BEZIER); g = GetBezierValue(time, i, G, curveType + BEZIER_SIZE - BEZIER); b = GetBezierValue(time, i, B, curveType + BEZIER_SIZE * 2 - BEZIER); r2 = GetBezierValue(time, i, R2, curveType + BEZIER_SIZE * 3 - BEZIER); g2 = GetBezierValue(time, i, G2, curveType + BEZIER_SIZE * 4 - BEZIER); b2 = GetBezierValue(time, i, B2, curveType + BEZIER_SIZE * 5 - BEZIER); break; } if (alpha == 1) { slot.r = r; slot.g = g; slot.b = b; slot.r2 = r2; slot.g2 = g2; slot.b2 = b2; } else { float br, bg, bb, br2, bg2, bb2; if (blend == MixBlend.Setup) { SlotData setup = slot.data; br = setup.r; bg = setup.g; bb = setup.b; br2 = setup.r2; bg2 = setup.g2; bb2 = setup.b2; } else { br = slot.r; bg = slot.g; bb = slot.b; br2 = slot.r2; bg2 = slot.g2; bb2 = slot.b2; } slot.r = br + (r - br) * alpha; slot.g = bg + (g - bg) * alpha; slot.b = bb + (b - bb) * alpha; slot.r2 = br2 + (r2 - br2) * alpha; slot.g2 = bg2 + (g2 - bg2) * alpha; slot.b2 = bb2 + (b2 - bb2) * alpha; } slot.ClampColor(); slot.ClampSecondColor(); } } /// Changes a slot's . public class AttachmentTimeline : Timeline, ISlotTimeline { readonly int slotIndex; readonly string[] attachmentNames; public AttachmentTimeline (int frameCount, int slotIndex) : base(frameCount, (int)Property.Attachment + "|" + slotIndex) { this.slotIndex = slotIndex; attachmentNames = new String[frameCount]; } public int SlotIndex { get { return slotIndex; } } /// The attachment name for each frame. May contain null values to clear the attachment. public string[] AttachmentNames { get { return attachmentNames; } } /// Sets the time and attachment name for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, String attachmentName) { frames[frame] = time; attachmentNames[frame] = attachmentName; } public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; if (direction == MixDirection.Out) { if (blend == MixBlend.Setup) SetAttachment(skeleton, slot, slot.data.attachmentName); return; } float[] frames = this.frames; if (time < frames[0]) { if (blend == MixBlend.Setup || blend == MixBlend.First) SetAttachment(skeleton, slot, slot.data.attachmentName); return; } SetAttachment(skeleton, slot, attachmentNames[Search(frames, time)]); } private void SetAttachment (Skeleton skeleton, Slot slot, string attachmentName) { slot.Attachment = attachmentName == null ? null : skeleton.GetAttachment(slotIndex, attachmentName); } } /// Changes a slot's to deform a . public class DeformTimeline : CurveTimeline, ISlotTimeline { readonly int slotIndex; readonly VertexAttachment attachment; internal float[][] vertices; public DeformTimeline (int frameCount, int bezierCount, int slotIndex, VertexAttachment attachment) : base(frameCount, bezierCount, (int)Property.Deform + "|" + slotIndex + "|" + attachment.Id) { this.slotIndex = slotIndex; this.attachment = attachment; vertices = new float[frameCount][]; } public int SlotIndex { get { return slotIndex; } } /// The attachment that will be deformed. /// public VertexAttachment Attachment { get { return attachment; } } /// The vertices for each frame. public float[][] Vertices { get { return vertices; } } /// Sets the time and vertices for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. /// Vertex positions for an unweighted VertexAttachment, or deform offsets if it has weights. public void SetFrame (int frame, float time, float[] vertices) { frames[frame] = time; this.vertices[frame] = vertices; } /// Ignored (0 is used for a deform timeline). /// Ignored (1 is used for a deform timeline). public void setBezier (int bezier, int frame, int value, float time1, float value1, float cx1, float cy1, float cx2, float cy2, float time2, float value2) { float[] curves = this.curves; int i = FrameCount + bezier * BEZIER_SIZE; if (value == 0) curves[frame] = BEZIER + i; float tmpx = (time1 - cx1 * 2 + cx2) * 0.03f, tmpy = cy2 * 0.03f - cy1 * 0.06f; float dddx = ((cx1 - cx2) * 3 - time1 + time2) * 0.006f, dddy = (cy1 - cy2 + 0.33333333f) * 0.018f; float ddx = tmpx * 2 + dddx, ddy = tmpy * 2 + dddy; float dx = (cx1 - time1) * 0.3f + tmpx + dddx * 0.16666667f, dy = cy1 * 0.3f + tmpy + dddy * 0.16666667f; float x = time1 + dx, y = dy; for (int n = i + BEZIER_SIZE; i < n; i += 2) { curves[i] = x; curves[i + 1] = y; dx += ddx; dy += ddy; ddx += dddx; ddy += dddy; x += dx; y += dy; } } /// Returns the interpolated percentage for the specified time. /// The frame before time. private float GetCurvePercent (float time, int frame) { float[] curves = this.curves; int i = (int)curves[frame]; switch (i) { case LINEAR: float x = frames[frame]; return (time - x) / (frames[frame + FrameEntries] - x); case STEPPED: return 0; } i -= BEZIER; if (curves[i] > time) { float x = frames[frame]; return curves[i + 1] * (time - x) / (curves[i] - x); } int n = i + BEZIER_SIZE; for (i += 2; i < n; i += 2) { if (curves[i] >= time) { float x = curves[i - 2], y = curves[i - 1]; return y + (time - x) / (curves[i] - x) * (curves[i + 1] - y); } } { // scope added to prevent compile error "float x and y declared in enclosing scope" float x = curves[n - 2], y = curves[n - 1]; return y + (1 - y) * (time - x) / (frames[frame + FrameEntries] - x); } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; VertexAttachment vertexAttachment = slot.attachment as VertexAttachment; if (vertexAttachment == null || vertexAttachment.TimelineAttachment != attachment) return; ExposedList deformArray = slot.deform; if (deformArray.Count == 0) blend = MixBlend.Setup; float[][] vertices = this.vertices; int vertexCount = vertices[0].Length; float[] deform; float[] frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.Setup: deformArray.Clear(); return; case MixBlend.First: if (alpha == 1) { deformArray.Clear(); return; } // Ensure size and preemptively set count. if (deformArray.Capacity < vertexCount) deformArray.Capacity = vertexCount; deformArray.Count = vertexCount; deform = deformArray.Items; if (vertexAttachment.bones == null) { // Unweighted vertex positions. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) deform[i] += (setupVertices[i] - deform[i]) * alpha; } else { // Weighted deform offsets. alpha = 1 - alpha; for (int i = 0; i < vertexCount; i++) deform[i] *= alpha; } return; } return; } // Ensure size and preemptively set count. if (deformArray.Capacity < vertexCount) deformArray.Capacity = vertexCount; deformArray.Count = vertexCount; deform = deformArray.Items; if (time >= frames[frames.Length - 1]) { // Time is after last frame. float[] lastVertices = vertices[frames.Length - 1]; if (alpha == 1) { if (blend == MixBlend.Add) { if (vertexAttachment.bones == null) { // Unweighted vertex positions, no alpha. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) deform[i] += lastVertices[i] - setupVertices[i]; } else { // Weighted deform offsets, no alpha. for (int i = 0; i < vertexCount; i++) deform[i] += lastVertices[i]; } } else { // Vertex positions or deform offsets, no alpha. Array.Copy(lastVertices, 0, deform, 0, vertexCount); } } else { switch (blend) { case MixBlend.Setup: { if (vertexAttachment.bones == null) { // Unweighted vertex positions, with alpha. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) { float setup = setupVertices[i]; deform[i] = setup + (lastVertices[i] - setup) * alpha; } } else { // Weighted deform offsets, with alpha. for (int i = 0; i < vertexCount; i++) deform[i] = lastVertices[i] * alpha; } break; } case MixBlend.First: case MixBlend.Replace: // Vertex positions or deform offsets, with alpha. for (int i = 0; i < vertexCount; i++) deform[i] += (lastVertices[i] - deform[i]) * alpha; break; case MixBlend.Add: if (vertexAttachment.bones == null) { // Unweighted vertex positions, no alpha. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) deform[i] += (lastVertices[i] - setupVertices[i]) * alpha; } else { // Weighted deform offsets, alpha. for (int i = 0; i < vertexCount; i++) deform[i] += lastVertices[i] * alpha; } break; } } return; } int frame = Search(frames, time); float percent = GetCurvePercent(time, frame); float[] prevVertices = vertices[frame]; float[] nextVertices = vertices[frame + 1]; if (alpha == 1) { if (blend == MixBlend.Add) { if (vertexAttachment.bones == null) { // Unweighted vertex positions, no alpha. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] += prev + (nextVertices[i] - prev) * percent - setupVertices[i]; } } else { // Weighted deform offsets, no alpha. for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] += prev + (nextVertices[i] - prev) * percent; } } } else { // Vertex positions or deform offsets, no alpha. for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] = prev + (nextVertices[i] - prev) * percent; } } } else { switch (blend) { case MixBlend.Setup: { if (vertexAttachment.bones == null) { // Unweighted vertex positions, with alpha. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i], setup = setupVertices[i]; deform[i] = setup + (prev + (nextVertices[i] - prev) * percent - setup) * alpha; } } else { // Weighted deform offsets, with alpha. for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] = (prev + (nextVertices[i] - prev) * percent) * alpha; } } break; } case MixBlend.First: case MixBlend.Replace: { // Vertex positions or deform offsets, with alpha. for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] += (prev + (nextVertices[i] - prev) * percent - deform[i]) * alpha; } break; } case MixBlend.Add: if (vertexAttachment.bones == null) { // Unweighted vertex positions, with alpha. float[] setupVertices = vertexAttachment.vertices; for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] += (prev + (nextVertices[i] - prev) * percent - setupVertices[i]) * alpha; } } else { // Weighted deform offsets, with alpha. for (int i = 0; i < vertexCount; i++) { float prev = prevVertices[i]; deform[i] += (prev + (nextVertices[i] - prev) * percent) * alpha; } } break; } } } } /// Fires an when specific animation times are reached. public class EventTimeline : Timeline { readonly static string[] propertyIds = { ((int)Property.Event).ToString() }; readonly Event[] events; public EventTimeline (int frameCount) : base(frameCount, propertyIds) { events = new Event[frameCount]; } /// The event for each frame. public Event[] Events { get { return events; } } /// Sets the time and event for the specified frame. /// Between 0 and frameCount, inclusive. public void SetFrame (int frame, Event e) { frames[frame] = e.time; events[frame] = e; } /// Fires events for frames > lastTime and <= time. public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { if (firedEvents == null) return; float[] frames = this.frames; int frameCount = frames.Length; if (lastTime > time) { // Apply after lastTime for looped animations. Apply(skeleton, lastTime, int.MaxValue, firedEvents, alpha, blend, direction); lastTime = -1f; } else if (lastTime >= frames[frameCount - 1]) // Last time is after last frame. return; if (time < frames[0]) return; int i; if (lastTime < frames[0]) i = 0; else { i = Search(frames, lastTime) + 1; float frameTime = frames[i]; while (i > 0) { // Fire multiple events with the same frame. if (frames[i - 1] != frameTime) break; i--; } } for (; i < frameCount && time >= frames[i]; i++) firedEvents.Add(events[i]); } } /// Changes a skeleton's . public class DrawOrderTimeline : Timeline { static readonly string[] propertyIds = { ((int)Property.DrawOrder).ToString() }; readonly int[][] drawOrders; public DrawOrderTimeline (int frameCount) : base(frameCount, propertyIds) { drawOrders = new int[frameCount][]; } /// The draw order for each frame. /// . public int[][] DrawOrders { get { return drawOrders; } } /// Sets the time and draw order for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. /// For each slot in , the index of the slot in the new draw order. May be null to use /// setup pose draw order. public void SetFrame (int frame, float time, int[] drawOrder) { frames[frame] = time; drawOrders[frame] = drawOrder; } public override void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { if (direction == MixDirection.Out) { if (blend == MixBlend.Setup) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); return; } float[] frames = this.frames; if (time < frames[0]) { if (blend == MixBlend.Setup || blend == MixBlend.First) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); return; } int[] drawOrderToSetupIndex = drawOrders[Search(frames, time)]; if (drawOrderToSetupIndex == null) Array.Copy(skeleton.slots.Items, 0, skeleton.drawOrder.Items, 0, skeleton.slots.Count); else { Slot[] slots = skeleton.slots.Items; Slot[] drawOrder = skeleton.drawOrder.Items; for (int i = 0, n = drawOrderToSetupIndex.Length; i < n; i++) drawOrder[i] = slots[drawOrderToSetupIndex[i]]; } } } /// Changes an IK constraint's , , /// , , and . public class IkConstraintTimeline : CurveTimeline { public const int ENTRIES = 6; private const int MIX = 1, SOFTNESS = 2, BEND_DIRECTION = 3, COMPRESS = 4, STRETCH = 5; readonly int constraintIndex; public IkConstraintTimeline (int frameCount, int bezierCount, int ikConstraintIndex) : base(frameCount, bezierCount, (int)Property.IkConstraint + "|" + ikConstraintIndex) { this.constraintIndex = ikConstraintIndex; } public override int FrameEntries { get { return ENTRIES; } } /// The index of the IK constraint in that will be changed when this timeline is /// applied. public int IkConstraintIndex { get { return constraintIndex; } } /// Sets the time, mix, softness, bend direction, compress, and stretch for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. /// 1 or -1. public void SetFrame (int frame, float time, float mix, float softness, int bendDirection, bool compress, bool stretch) { frame *= ENTRIES; frames[frame] = time; frames[frame + MIX] = mix; frames[frame + SOFTNESS] = softness; frames[frame + BEND_DIRECTION] = bendDirection; frames[frame + COMPRESS] = compress ? 1 : 0; frames[frame + STRETCH] = stretch ? 1 : 0; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { IkConstraint constraint = skeleton.ikConstraints.Items[constraintIndex]; if (!constraint.active) return; float[] frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.Setup: constraint.mix = constraint.data.mix; constraint.softness = constraint.data.softness; constraint.bendDirection = constraint.data.bendDirection; constraint.compress = constraint.data.compress; constraint.stretch = constraint.data.stretch; return; case MixBlend.First: constraint.mix += (constraint.data.mix - constraint.mix) * alpha; constraint.softness += (constraint.data.softness - constraint.softness) * alpha; constraint.bendDirection = constraint.data.bendDirection; constraint.compress = constraint.data.compress; constraint.stretch = constraint.data.stretch; return; } return; } float mix, softness; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; mix = frames[i + MIX]; softness = frames[i + SOFTNESS]; float t = (time - before) / (frames[i + ENTRIES] - before); mix += (frames[i + ENTRIES + MIX] - mix) * t; softness += (frames[i + ENTRIES + SOFTNESS] - softness) * t; break; case STEPPED: mix = frames[i + MIX]; softness = frames[i + SOFTNESS]; break; default: mix = GetBezierValue(time, i, MIX, curveType - BEZIER); softness = GetBezierValue(time, i, SOFTNESS, curveType + BEZIER_SIZE - BEZIER); break; } if (blend == MixBlend.Setup) { constraint.mix = constraint.data.mix + (mix - constraint.data.mix) * alpha; constraint.softness = constraint.data.softness + (softness - constraint.data.softness) * alpha; if (direction == MixDirection.Out) { constraint.bendDirection = constraint.data.bendDirection; constraint.compress = constraint.data.compress; constraint.stretch = constraint.data.stretch; } else { constraint.bendDirection = (int)frames[i + BEND_DIRECTION]; constraint.compress = frames[i + COMPRESS] != 0; constraint.stretch = frames[i + STRETCH] != 0; } } else { constraint.mix += (mix - constraint.mix) * alpha; constraint.softness += (softness - constraint.softness) * alpha; if (direction == MixDirection.In) { constraint.bendDirection = (int)frames[i + BEND_DIRECTION]; constraint.compress = frames[i + COMPRESS] != 0; constraint.stretch = frames[i + STRETCH] != 0; } } } } /// Changes a transform constraint's mixes. public class TransformConstraintTimeline : CurveTimeline { public const int ENTRIES = 7; private const int ROTATE = 1, X = 2, Y = 3, SCALEX = 4, SCALEY = 5, SHEARY = 6; readonly int constraintIndex; public TransformConstraintTimeline (int frameCount, int bezierCount, int transformConstraintIndex) : base(frameCount, bezierCount, (int)Property.TransformConstraint + "|" + transformConstraintIndex) { constraintIndex = transformConstraintIndex; } public override int FrameEntries { get { return ENTRIES; } } /// The index of the transform constraint in that will be changed when this /// timeline is applied. public int TransformConstraintIndex { get { return constraintIndex; } } /// Sets the time, rotate mix, translate mix, scale mix, and shear mix for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float mixRotate, float mixX, float mixY, float mixScaleX, float mixScaleY, float mixShearY) { frame *= ENTRIES; frames[frame] = time; frames[frame + ROTATE] = mixRotate; frames[frame + X] = mixX; frames[frame + Y] = mixY; frames[frame + SCALEX] = mixScaleX; frames[frame + SCALEY] = mixScaleY; frames[frame + SHEARY] = mixShearY; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { TransformConstraint constraint = skeleton.transformConstraints.Items[constraintIndex]; if (!constraint.active) return; float[] frames = this.frames; if (time < frames[0]) { TransformConstraintData data = constraint.data; switch (blend) { case MixBlend.Setup: constraint.mixRotate = data.mixRotate; constraint.mixX = data.mixX; constraint.mixY = data.mixY; constraint.mixScaleX = data.mixScaleX; constraint.mixScaleY = data.mixScaleY; constraint.mixShearY = data.mixShearY; return; case MixBlend.First: constraint.mixRotate += (data.mixRotate - constraint.mixRotate) * alpha; constraint.mixX += (data.mixX - constraint.mixX) * alpha; constraint.mixY += (data.mixY - constraint.mixY) * alpha; constraint.mixScaleX += (data.mixScaleX - constraint.mixScaleX) * alpha; constraint.mixScaleY += (data.mixScaleY - constraint.mixScaleY) * alpha; constraint.mixShearY += (data.mixShearY - constraint.mixShearY) * alpha; return; } return; } float rotate, x, y, scaleX, scaleY, shearY; GetCurveValue(out rotate, out x, out y, out scaleX, out scaleY, out shearY, time); if (blend == MixBlend.Setup) { TransformConstraintData data = constraint.data; constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; constraint.mixX = data.mixX + (x - data.mixX) * alpha; constraint.mixY = data.mixY + (y - data.mixY) * alpha; constraint.mixScaleX = data.mixScaleX + (scaleX - data.mixScaleX) * alpha; constraint.mixScaleY = data.mixScaleY + (scaleY - data.mixScaleY) * alpha; constraint.mixShearY = data.mixShearY + (shearY - data.mixShearY) * alpha; } else { constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; constraint.mixX += (x - constraint.mixX) * alpha; constraint.mixY += (y - constraint.mixY) * alpha; constraint.mixScaleX += (scaleX - constraint.mixScaleX) * alpha; constraint.mixScaleY += (scaleY - constraint.mixScaleY) * alpha; constraint.mixShearY += (shearY - constraint.mixShearY) * alpha; } } public void GetCurveValue (out float rotate, out float x, out float y, out float scaleX, out float scaleY, out float shearY, float time) { float[] frames = this.frames; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i / ENTRIES]; switch (curveType) { case LINEAR: float before = frames[i]; rotate = frames[i + ROTATE]; x = frames[i + X]; y = frames[i + Y]; scaleX = frames[i + SCALEX]; scaleY = frames[i + SCALEY]; shearY = frames[i + SHEARY]; float t = (time - before) / (frames[i + ENTRIES] - before); rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t; x += (frames[i + ENTRIES + X] - x) * t; y += (frames[i + ENTRIES + Y] - y) * t; scaleX += (frames[i + ENTRIES + SCALEX] - scaleX) * t; scaleY += (frames[i + ENTRIES + SCALEY] - scaleY) * t; shearY += (frames[i + ENTRIES + SHEARY] - shearY) * t; break; case STEPPED: rotate = frames[i + ROTATE]; x = frames[i + X]; y = frames[i + Y]; scaleX = frames[i + SCALEX]; scaleY = frames[i + SCALEY]; shearY = frames[i + SHEARY]; break; default: rotate = GetBezierValue(time, i, ROTATE, curveType - BEZIER); x = GetBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER); y = GetBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER); scaleX = GetBezierValue(time, i, SCALEX, curveType + BEZIER_SIZE * 3 - BEZIER); scaleY = GetBezierValue(time, i, SCALEY, curveType + BEZIER_SIZE * 4 - BEZIER); shearY = GetBezierValue(time, i, SHEARY, curveType + BEZIER_SIZE * 5 - BEZIER); break; } } } /// Changes a path constraint's . public class PathConstraintPositionTimeline : CurveTimeline1 { readonly int constraintIndex; public PathConstraintPositionTimeline (int frameCount, int bezierCount, int pathConstraintIndex) : base(frameCount, bezierCount, (int)Property.PathConstraintPosition + "|" + pathConstraintIndex) { this.constraintIndex = pathConstraintIndex; } /// The index of the path constraint slot in that will be changed when this timeline /// is applied. public int PathConstraintIndex { get { return constraintIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { PathConstraint constraint = skeleton.pathConstraints.Items[constraintIndex]; if (constraint.active) constraint.position = GetAbsoluteValue(time, alpha, blend, constraint.position, constraint.data.position); } } /// Changes a path constraint's . public class PathConstraintSpacingTimeline : CurveTimeline1 { readonly int constraintIndex; public PathConstraintSpacingTimeline (int frameCount, int bezierCount, int pathConstraintIndex) : base(frameCount, bezierCount, (int)Property.PathConstraintSpacing + "|" + pathConstraintIndex) { constraintIndex = pathConstraintIndex; } /// The index of the path constraint in that will be changed when this timeline /// is applied. public int PathConstraintIndex { get { return constraintIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList events, float alpha, MixBlend blend, MixDirection direction) { PathConstraint constraint = skeleton.pathConstraints.Items[constraintIndex]; if (constraint.active) constraint.spacing = GetAbsoluteValue(time, alpha, blend, constraint.spacing, constraint.data.spacing); } } /// Changes a path constraint's , , and /// . public class PathConstraintMixTimeline : CurveTimeline { public const int ENTRIES = 4; private const int ROTATE = 1, X = 2, Y = 3; readonly int constraintIndex; public PathConstraintMixTimeline (int frameCount, int bezierCount, int pathConstraintIndex) : base(frameCount, bezierCount, (int)Property.PathConstraintMix + "|" + pathConstraintIndex) { constraintIndex = pathConstraintIndex; } public override int FrameEntries { get { return ENTRIES; } } /// The index of the path constraint slot in that will be changed when this timeline /// is applied. public int PathConstraintIndex { get { return constraintIndex; } } /// Sets the time and color for the specified frame. /// Between 0 and frameCount, inclusive. /// The frame time in seconds. public void SetFrame (int frame, float time, float mixRotate, float mixX, float mixY) { frame <<= 2; frames[frame] = time; frames[frame + ROTATE] = mixRotate; frames[frame + X] = mixX; frames[frame + Y] = mixY; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { PathConstraint constraint = skeleton.pathConstraints.Items[constraintIndex]; if (!constraint.active) return; float[] frames = this.frames; if (time < frames[0]) { switch (blend) { case MixBlend.Setup: constraint.mixRotate = constraint.data.mixRotate; constraint.mixX = constraint.data.mixX; constraint.mixY = constraint.data.mixY; return; case MixBlend.First: constraint.mixRotate += (constraint.data.mixRotate - constraint.mixRotate) * alpha; constraint.mixX += (constraint.data.mixX - constraint.mixX) * alpha; constraint.mixY += (constraint.data.mixY - constraint.mixY) * alpha; return; } return; } float rotate, x, y; int i = Search(frames, time, ENTRIES), curveType = (int)curves[i >> 2]; switch (curveType) { case LINEAR: float before = frames[i]; rotate = frames[i + ROTATE]; x = frames[i + X]; y = frames[i + Y]; float t = (time - before) / (frames[i + ENTRIES] - before); rotate += (frames[i + ENTRIES + ROTATE] - rotate) * t; x += (frames[i + ENTRIES + X] - x) * t; y += (frames[i + ENTRIES + Y] - y) * t; break; case STEPPED: rotate = frames[i + ROTATE]; x = frames[i + X]; y = frames[i + Y]; break; default: rotate = GetBezierValue(time, i, ROTATE, curveType - BEZIER); x = GetBezierValue(time, i, X, curveType + BEZIER_SIZE - BEZIER); y = GetBezierValue(time, i, Y, curveType + BEZIER_SIZE * 2 - BEZIER); break; } if (blend == MixBlend.Setup) { PathConstraintData data = constraint.data; constraint.mixRotate = data.mixRotate + (rotate - data.mixRotate) * alpha; constraint.mixX = data.mixX + (x - data.mixX) * alpha; constraint.mixY = data.mixY + (y - data.mixY) * alpha; } else { constraint.mixRotate += (rotate - constraint.mixRotate) * alpha; constraint.mixX += (x - constraint.mixX) * alpha; constraint.mixY += (y - constraint.mixY) * alpha; } } } /// The base class for most timelines. public abstract class PhysicsConstraintTimeline : CurveTimeline1 { readonly int constraintIndex; /// -1 for all physics constraints in the skeleton. public PhysicsConstraintTimeline (int frameCount, int bezierCount, int physicsConstraintIndex, Property property) : base(frameCount, bezierCount, (int)property + "|" + physicsConstraintIndex) { constraintIndex = physicsConstraintIndex; } /// The index of the physics constraint in that will be changed when this timeline /// is applied, or -1 if all physics constraints in the skeleton will be changed. public int PhysicsConstraintIndex { get { return constraintIndex; } } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { PhysicsConstraint constraint; if (constraintIndex == -1) { float value = time >= frames[0] ? GetCurveValue(time) : 0; PhysicsConstraint[] constraints = skeleton.physicsConstraints.Items; for (int i = 0, n = skeleton.physicsConstraints.Count; i < n; i++) { constraint = (PhysicsConstraint)constraints[i]; if (constraint.active && Global(constraint.data)) Set(constraint, GetAbsoluteValue(time, alpha, blend, Get(constraint), Setup(constraint), value)); } } else { constraint = skeleton.physicsConstraints.Items[constraintIndex]; if (constraint.active) Set(constraint, GetAbsoluteValue(time, alpha, blend, Get(constraint), Setup(constraint))); } } abstract protected float Setup (PhysicsConstraint constraint); abstract protected float Get (PhysicsConstraint constraint); abstract protected void Set (PhysicsConstraint constraint, float value); abstract protected bool Global (PhysicsConstraintData constraint); } /// Changes a physics constraint's . public class PhysicsConstraintInertiaTimeline : PhysicsConstraintTimeline { public PhysicsConstraintInertiaTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintInertia) { } override protected float Setup (PhysicsConstraint constraint) { return constraint.data.inertia; } override protected float Get (PhysicsConstraint constraint) { return constraint.inertia; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.inertia = value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.inertiaGlobal; } } /// Changes a physics constraint's . public class PhysicsConstraintStrengthTimeline : PhysicsConstraintTimeline { public PhysicsConstraintStrengthTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintStrength) { } override protected float Setup (PhysicsConstraint constraint) { return constraint.data.strength; } override protected float Get (PhysicsConstraint constraint) { return constraint.strength; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.strength = value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.strengthGlobal; } } /// Changes a physics constraint's . public class PhysicsConstraintDampingTimeline : PhysicsConstraintTimeline { public PhysicsConstraintDampingTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintDamping) { } override protected float Setup (PhysicsConstraint constraint) { return constraint.data.damping; } override protected float Get (PhysicsConstraint constraint) { return constraint.damping; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.damping = value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.dampingGlobal; } } /// Changes a physics constraint's . The timeline values are not inverted. public class PhysicsConstraintMassTimeline : PhysicsConstraintTimeline { public PhysicsConstraintMassTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintMass) { } override protected float Setup (PhysicsConstraint constraint) { return 1 / constraint.data.massInverse; } override protected float Get (PhysicsConstraint constraint) { return 1 / constraint.massInverse; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.massInverse = 1 / value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.massGlobal; } } /// Changes a physics constraint's . public class PhysicsConstraintWindTimeline : PhysicsConstraintTimeline { public PhysicsConstraintWindTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintWind) { } override protected float Setup (PhysicsConstraint constraint) { return constraint.data.wind; } override protected float Get (PhysicsConstraint constraint) { return constraint.wind; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.wind = value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.windGlobal; } } /// Changes a physics constraint's . public class PhysicsConstraintGravityTimeline : PhysicsConstraintTimeline { public PhysicsConstraintGravityTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintGravity) { } override protected float Setup (PhysicsConstraint constraint) { return constraint.data.gravity; } override protected float Get (PhysicsConstraint constraint) { return constraint.gravity; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.gravity = value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.gravityGlobal; } } /// Changes a physics constraint's . public class PhysicsConstraintMixTimeline : PhysicsConstraintTimeline { public PhysicsConstraintMixTimeline (int frameCount, int bezierCount, int physicsConstraintIndex) : base(frameCount, bezierCount, physicsConstraintIndex, Property.PhysicsConstraintMix) { } override protected float Setup (PhysicsConstraint constraint) { return constraint.data.mix; } override protected float Get (PhysicsConstraint constraint) { return constraint.mix; } override protected void Set (PhysicsConstraint constraint, float value) { constraint.mix = value; } override protected bool Global (PhysicsConstraintData constraint) { return constraint.mixGlobal; } } /// Resets a physics constraint when specific animation times are reached. public class PhysicsConstraintResetTimeline : Timeline { static readonly string[] propertyIds = { ((int)Property.PhysicsConstraintReset).ToString() }; readonly int constraintIndex; /// -1 for all physics constraints in the skeleton. public PhysicsConstraintResetTimeline (int frameCount, int physicsConstraintIndex) : base(frameCount, propertyIds) { constraintIndex = physicsConstraintIndex; } /// The index of the physics constraint in that will be reset when this timeline is /// applied, or -1 if all physics constraints in the skeleton will be reset. public int PhysicsConstraintIndex { get { return constraintIndex; } } override public int FrameCount { get { return frames.Length; } } /// Sets the time for the specified frame. /// Between 0 and frameCount, inclusive. public void SetFrame (int frame, float time) { frames[frame] = time; } /// Resets the physics constraint when frames > lastTime and <= time. override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { PhysicsConstraint constraint = null; if (constraintIndex != -1) { constraint = skeleton.physicsConstraints.Items[constraintIndex]; if (!constraint.active) return; } float[] frames = this.frames; if (lastTime > time) { // Apply after lastTime for looped animations. Apply(skeleton, lastTime, int.MaxValue, null, alpha, blend, direction); lastTime = -1f; } else if (lastTime >= frames[frames.Length - 1]) // Last time is after last frame. return; if (time < frames[0]) return; if (lastTime < frames[0] || time >= frames[Search(frames, lastTime) + 1]) { if (constraint != null) constraint.Reset(); else { PhysicsConstraint[] constraints = skeleton.physicsConstraints.Items; for (int i = 0, n = skeleton.physicsConstraints.Count; i < n; i++) { constraint = (PhysicsConstraint)constraints[i]; if (constraint.active) constraint.Reset(); } } } } } /// Changes a slot's for an attachment's . public class SequenceTimeline : Timeline, ISlotTimeline { public const int ENTRIES = 3; private const int MODE = 1, DELAY = 2; readonly int slotIndex; readonly IHasTextureRegion attachment; public SequenceTimeline (int frameCount, int slotIndex, Attachment attachment) : base(frameCount, (int)Property.Sequence + "|" + slotIndex + "|" + ((IHasTextureRegion)attachment).Sequence.Id) { this.slotIndex = slotIndex; this.attachment = (IHasTextureRegion)attachment; } public override int FrameEntries { get { return ENTRIES; } } public int SlotIndex { get { return slotIndex; } } public Attachment Attachment { get { return (Attachment)attachment; } } /// Sets the time, mode, index, and frame time for the specified frame. /// Between 0 and frameCount, inclusive. /// Seconds between frames. public void SetFrame (int frame, float time, SequenceMode mode, int index, float delay) { frame *= ENTRIES; frames[frame] = time; frames[frame + MODE] = (int)mode | (index << 4); frames[frame + DELAY] = delay; } override public void Apply (Skeleton skeleton, float lastTime, float time, ExposedList firedEvents, float alpha, MixBlend blend, MixDirection direction) { Slot slot = skeleton.slots.Items[slotIndex]; if (!slot.bone.active) return; Attachment slotAttachment = slot.attachment; if (slotAttachment != attachment) { VertexAttachment vertexAttachment = slotAttachment as VertexAttachment; if ((vertexAttachment == null) || vertexAttachment.TimelineAttachment != attachment) return; } Sequence sequence = ((IHasTextureRegion)slotAttachment).Sequence; if (sequence == null) return; float[] frames = this.frames; if (time < frames[0]) { if (blend == MixBlend.Setup || blend == MixBlend.First) slot.SequenceIndex = -1; return; } int i = Search(frames, time, ENTRIES); float before = frames[i]; int modeAndIndex = (int)frames[i + MODE]; float delay = frames[i + DELAY]; int index = modeAndIndex >> 4, count = sequence.Regions.Length; SequenceMode mode = (SequenceMode)(modeAndIndex & 0xf); if (mode != SequenceMode.Hold) { index += (int)((time - before) / delay + 0.0001f); switch (mode) { case SequenceMode.Once: index = Math.Min(count - 1, index); break; case SequenceMode.Loop: index %= count; break; case SequenceMode.Pingpong: { int n = (count << 1) - 2; index = n == 0 ? 0 : index % n; if (index >= count) index = n - index; break; } case SequenceMode.OnceReverse: index = Math.Max(count - 1 - index, 0); break; case SequenceMode.LoopReverse: index = count - 1 - (index % count); break; case SequenceMode.PingpongReverse: { int n = (count << 1) - 2; index = n == 0 ? 0 : (index + count - 1) % n; if (index >= count) index = n - index; break; } // end case } } slot.SequenceIndex = index; } } }