OverviewFilesCommitsIssuesDocsReleasesPeers

ifsrenderer-linux / IFSEngine / Animation / Easing.cs

112 lines · csharp ref:
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));
    }
}