using System.Data; namespace RhythmBase.Components.Easing { /// /// Represents a value that changes over time according to a series of easing nodes. /// /// /// Initializes a new instance of the class with the specified original value and easing nodes. /// /// The original value before any easing is applied. /// The collection of easing nodes that define how the value changes over time. public struct EaseValue(float originalValue, IEnumerable nodes) { /// /// Gets or sets the collection of easing nodes, ordered by their start time. /// public EaseNode[] Nodes { get; set; } = [.. nodes.OrderBy(x => x.Time)]; /// /// Gets or sets the original value before any easing is applied. /// public float OriginalValue { get; set; } = originalValue; /// /// Gets the value at the specified time, taking into account the easing nodes. /// /// The time at which to get the value. /// The value at the specified time. public readonly float GetValue(float time) { if (Nodes.Length == 0 || time <= Nodes[0].Time) return OriginalValue; int i = Nodes.Length - 1; while (i >= 0 && time <= Nodes[i].Time) i--; if (Nodes[i].Time + Nodes[i].Duration <= time) return Nodes[i].Target; Stack ns = new([Nodes[i]]); while (--i > -1) { if (IsInRange(Nodes[i], ns.Peek().Time)) { ns.Push(Nodes[i]); } } float origin = i == -1 ? OriginalValue : Nodes[i + 1].Target; while (ns.Count > 1) { EaseNode node = ns.Pop(); origin = GetValue(node, ns.Peek().Time, origin); } origin = GetValue(ns.Pop(), time, origin); return origin; } /// /// Determines whether the specified time is within the range of the easing node. /// /// The easing node to check. /// The time to check. /// true if the time is within the range of the easing node; otherwise, false. private static bool IsInRange(EaseNode node, float time) => node.Time <= time && time <= node.Time + node.Duration; /// /// Calculates the value of the easing node at the specified time. /// /// The easing node to calculate the value for. /// The time at which to calculate the value. /// The original value before any easing is applied. /// The calculated value at the specified time. private static float GetValue(EaseNode node, float time, float origin) => node.Duration == 0 ? node.Target : (float)node.Type.Calculate((time - node.Time) / node.Duration, origin, node.Target); /// /// Fits the data points to an easing function with the specified precision. /// /// The original value before any easing is applied. /// The array of time and value pairs to fit. /// The precision for fitting the data points. /// An instance that fits the data points. public static EaseValue Fit(float originalValue, (float time, float value)[] values, float precision = 3f) => Fit(originalValue, values, eases, precision); /// /// Fits the data points to an easing function with the specified precision. /// /// The original value before any easing is applied. /// The array of time and value pairs to fit. /// The precision for fitting the data points. /// The array of easing types to consider. /// An instance that fits the data points. public static EaseValue Fit(float originalValue, (float time, float value)[] values, EaseType[] easeTypes, float precision = 3f) { values = [.. values.OrderBy(x => x.time)]; if (values.Length == 0) return new EaseValue(originalValue, []); if (values.Length == 1) return new EaseValue(originalValue, [new EaseNode(values[0].value) { Duration = values[0].time, Type = EaseType.InOutSine }]); if (values.Length == 2) return new EaseValue(originalValue, [new EaseNode(values[0].value) { Time = values[0].time, Duration = values[1].time, Type = EaseType.InOutSine }]); return Fit([(0, originalValue), .. values], easeTypes, precision); } /// /// Fits the data points to an easing function with the specified precision. /// /// The array of time and value pairs to fit. /// The precision for fitting the data points. /// An instance that fits the data points. public static EaseValue Fit((float time, float value)[] values, float precision = 3f) => Fit(values, eases, precision); /// /// Fits the data points to an easing function with the specified precision and ease types. /// /// The array of time and value pairs to fit. /// The array of easing types to consider. /// The precision for fitting the data points. /// Special thanks to mfgujhgh for the algorithm! /// An instance that fits the data points. public static EaseValue Fit((float time, float value)[] values, EaseType[] easeTypes, float precision = 3f) { if (easeTypes.Length == 0) throw new ArgumentException("At least one ease type must be specified.", nameof(easeTypes)); values = [.. values.OrderBy(x => x.time)]; int[] steps = new int[values.Length], p = new int[values.Length]; EaseType[] pe = new EaseType[values.Length]; steps[0] = 0; for (int i = 1; i < values.Length; i++) { steps[i] = steps[i - 1] + 1; p[i] = i - 1; pe[i] = easeTypes[0]; for (int j = 0; j < i - 1; j++) { if (steps[j] + 1 >= steps[i]) continue; EaseType besttype = easeTypes[0]; float bests = float.MaxValue; foreach (var e in easeTypes) { float s = Check(values, j, i, e, precision); if (s < bests) { bests = s; besttype = e; } } if (bests < precision && steps[i] > steps[j] + 1) { steps[i] = steps[j] + 1; p[i] = j; pe[i] = besttype; } } } EaseNode[] ns = new EaseNode[steps[^1]]; int k = values.Length - 1; while (k != 0) { ns[--steps[k]] = new EaseNode(values[k].value) { Time = values[p[k]].time, Duration = values[k].time - values[p[k]].time, Type = pe[k] }; k = p[k]; } return new EaseValue(values[0].value, ns); } private static float Check((float time, float value)[] data, int start, int end, EaseType type, float precision) { float s = 0; for (int i = start + 1; i <= end; i++) { float v = data[start].value + (float)type.Calculate((data[i].time - data[start].time) / (data[end].time - data[start].time)) * (data[end].value - data[start].value); s = Math.Max(s, Math.Abs(v - data[i].value)); if (s > precision) { return s; } } return s; } private static readonly EaseType[] eases = Enum.GetValues(); } }