This repository has been archived on 2025-10-18. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files

181 lines
7.5 KiB
C#

using System.Data;
namespace RhythmBase.Components.Easing
{
/// <summary>
/// Represents a value that changes over time according to a series of easing nodes.
/// </summary>
/// <remarks>
/// Initializes a new instance of the <see cref="EaseValue"/> class with the specified original value and easing nodes.
/// </remarks>
/// <param name="originalValue">The original value before any easing is applied.</param>
/// <param name="nodes">The collection of easing nodes that define how the value changes over time.</param>
public struct EaseValue(float originalValue, IEnumerable<EaseNode> nodes)
{
/// <summary>
/// Gets or sets the collection of easing nodes, ordered by their start time.
/// </summary>
public EaseNode[] Nodes { get; set; } = [.. nodes.OrderBy(x => x.Time)];
/// <summary>
/// Gets or sets the original value before any easing is applied.
/// </summary>
public float OriginalValue { get; set; } = originalValue;
/// <summary>
/// Gets the value at the specified time, taking into account the easing nodes.
/// </summary>
/// <param name="time">The time at which to get the value.</param>
/// <returns>The value at the specified time.</returns>
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<EaseNode> 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;
}
/// <summary>
/// Determines whether the specified time is within the range of the easing node.
/// </summary>
/// <param name="node">The easing node to check.</param>
/// <param name="time">The time to check.</param>
/// <returns><c>true</c> if the time is within the range of the easing node; otherwise, <c>false</c>.</returns>
private static bool IsInRange(EaseNode node, float time) => node.Time <= time && time <= node.Time + node.Duration;
/// <summary>
/// Calculates the value of the easing node at the specified time.
/// </summary>
/// <param name="node">The easing node to calculate the value for.</param>
/// <param name="time">The time at which to calculate the value.</param>
/// <param name="origin">The original value before any easing is applied.</param>
/// <returns>The calculated value at the specified time.</returns>
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);
/// <summary>
/// Fits the data points to an easing function with the specified precision.
/// </summary>
/// <param name="originalValue">The original value before any easing is applied.</param>
/// <param name="values">The array of time and value pairs to fit.</param>
/// <param name="precision">The precision for fitting the data points.</param>
/// <returns>An <see cref="EaseValue"/> instance that fits the data points.</returns>
public static EaseValue Fit(float originalValue, (float time, float value)[] values, float precision = 3f) => Fit(originalValue, values, eases, precision);
/// <summary>
/// Fits the data points to an easing function with the specified precision.
/// </summary>
/// <param name="originalValue">The original value before any easing is applied.</param>
/// <param name="values">The array of time and value pairs to fit.</param>
/// <param name="precision">The precision for fitting the data points.</param>
/// <param name="easeTypes">The array of easing types to consider.</param>
/// <returns>An <see cref="EaseValue"/> instance that fits the data points.</returns>
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);
}
/// <summary>
/// Fits the data points to an easing function with the specified precision.
/// </summary>
/// <param name="values">The array of time and value pairs to fit.</param>
/// <param name="precision">The precision for fitting the data points.</param>
/// <returns>An <see cref="EaseValue"/> instance that fits the data points.</returns>
public static EaseValue Fit((float time, float value)[] values, float precision = 3f) => Fit(values, eases, precision);
/// <summary>
/// Fits the data points to an easing function with the specified precision and ease types.
/// </summary>
/// <param name="values">The array of time and value pairs to fit.</param>
/// <param name="easeTypes">The array of easing types to consider.</param>
/// <param name="precision">The precision for fitting the data points.</param>
/// <remarks>Special thanks to <b>mfgujhgh</b> for the algorithm!</remarks>
/// <returns>An <see cref="EaseValue"/> instance that fits the data points.</returns>
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<EaseType>();
}
}