using System;
namespace IFSEngine.Animation;
/// <summary>
/// The easing equations behind <see cref="KeyframeType"/>'s "Easing (by strength)" and
/// "Dynamic Effects" groups - the Penner set Blender also uses.
/// </summary>
/// <remarks>
/// Every function here maps [0,1] to a normalized progress with <c>f(0) = 0</c> and
/// <c>f(1) = 1</c>. That property is not decoration: violating it is exactly the bug
/// <see cref="EasingDirection.Out"/> shipped with (<c>1 - t^p</c> is 1 at t=0), which ran
/// every segment backwards. It is asserted per type in the test suite.
/// </remarks>
internal static class Easing
{
/// <summary>
/// Applies <paramref name="type"/> in <paramref name="direction"/> to a normalized
/// segment parameter.
/// </summary>
public static double Apply(KeyframeType type, EasingDirection direction, double t, Keyframe key)
{
t = Math.Clamp(t, 0, 1);
return direction switch
{
EasingDirection.In => In(type, t, key),
EasingDirection.Out => 1.0 - In(type, 1.0 - t, key),
// In over the first half, Out over the second, each at half scale.
EasingDirection.InOut => t < 0.5
? In(type, t * 2.0, key) / 2.0
: 1.0 - In(type, 2.0 - t * 2.0, key) / 2.0,
_ => t,
};
}
private static double In(KeyframeType type, double t, Keyframe key) => type switch
{
KeyframeType.Sinusoidal => 1.0 - Math.Cos(t * Math.PI / 2.0),
KeyframeType.Quadratic => t * t,
KeyframeType.Cubic => t * t * t,
KeyframeType.Quartic => t * t * t * t,
KeyframeType.Quintic => t * t * t * t * t,
KeyframeType.Exponential => ExponentialIn(t),
KeyframeType.Circular => 1.0 - Math.Sqrt(Math.Max(1.0 - t * t, 0)),
KeyframeType.Back => BackIn(t, key.BackOvershoot),
KeyframeType.Bounce => 1.0 - BounceOut(1.0 - t),
KeyframeType.Elastic => ElasticIn(t, key.ElasticAmplitude, key.ElasticPeriod),
_ => t,
};
/// <summary>
/// The classic exponential ramp, normalized so that f(0) is exactly 0.
/// </summary>
/// <remarks>
/// Penner's <c>2^(10(t-1))</c> is 0.0009765625 at t=0, not 0, and the usual implementations
/// paper over it with a special case at exactly zero - which leaves the curve DISCONTINUOUS
/// a hair's breadth into the segment. Subtracting that offset and rescaling makes it start
/// where its keyframe does without changing the shape anywhere it matters.
/// </remarks>
private static double ExponentialIn(double t)
{
if (t <= 0)
return 0;
const double floor = 1.0 / 1024.0; // 2^-10, the value at t = 0
return (Math.Pow(2, 10 * (t - 1)) - floor) / (1.0 - floor);
}
/// <summary>Overshoots backwards before running forward. <paramref name="s"/> is the overshoot.</summary>
private static double BackIn(double t, double s) => t * t * ((s + 1.0) * t - s);
private static double BounceOut(double t)
{
const double n = 7.5625, d = 2.75;
if (t < 1.0 / d)
return n * t * t;
if (t < 2.0 / d)
{
t -= 1.5 / d;
return n * t * t + 0.75;
}
if (t < 2.5 / d)
{
t -= 2.25 / d;
return n * t * t + 0.9375;
}
t -= 2.625 / d;
return n * t * t + 0.984375;
}
/// <summary>
/// A decaying oscillation. Zero <paramref name="amplitude"/> or <paramref name="period"/>
/// mean "auto", which is Penner's default of a unit amplitude over a 0.3 period.
/// </summary>
private static double ElasticIn(double t, double amplitude, double period)
{
if (t <= 0)
return 0;
if (t >= 1)
return 1;
double p = period > 0 ? period : 0.3;
double a = amplitude > 0 ? amplitude : 1.0;
// The phase shift that puts f(1) = 1 for the chosen amplitude.
double s = a < 1.0 ? p / 4.0 : p / (2.0 * Math.PI) * Math.Asin(Math.Clamp(1.0 / a, -1, 1));
if (a < 1.0)
a = 1.0;
return -(a * Math.Pow(2, 10 * (t - 1)) * Math.Sin((t - 1 - s) * 2.0 * Math.PI / p));
}
}