using System; using UnityEngine; namespace Uigenx.Animation { /// /// The 23 easing keywords USS transitions accept, plus for an explicit /// cubic bezier. Wire names are the kebab-case USS spellings. /// public enum UgxEasingKind { Ease = 0, EaseIn, EaseOut, EaseInOut, Linear, EaseInSine, EaseOutSine, EaseInOutSine, EaseInCubic, EaseOutCubic, EaseInOutCubic, EaseInCirc, EaseOutCirc, EaseInOutCirc, EaseInElastic, EaseOutElastic, EaseInOutElastic, EaseInBack, EaseOutBack, EaseInOutBack, EaseInBounce, EaseOutBounce, EaseInOutBounce, Bezier, } /// An easing keyword, or a cubic bezier when is Bezier. [Serializable] public struct UgxEasingCurve { [SerializeField] UgxEasingKind kind; [SerializeField] float x1, y1, x2, y2; public UgxEasingKind Kind => kind; public static UgxEasingCurve Of(UgxEasingKind kind) => new UgxEasingCurve { kind = kind }; public static UgxEasingCurve CubicBezier(float x1, float y1, float x2, float y2) => new UgxEasingCurve { kind = UgxEasingKind.Bezier, x1 = x1, y1 = y1, x2 = x2, y2 = y2 }; public static UgxEasingCurve Default => Of(UgxEasingKind.Ease); public float Evaluate(float t) => kind == UgxEasingKind.Bezier ? UgxEasing.Bezier(t, x1, y1, x2, y2) : UgxEasing.Evaluate(kind, t); } /// /// Easing math shared by the animator and the studio canvas. The keyword curves are the same /// Penner set Unity uses for USS transitions, so a clip looks identical in both. /// public static class UgxEasing { const float BackConstant = 1.70158f; const float ElasticPeriod = 0.3f; public static bool TryParse(string wireName, out UgxEasingKind kind) { switch (wireName) { case "ease": kind = UgxEasingKind.Ease; return true; case "ease-in": kind = UgxEasingKind.EaseIn; return true; case "ease-out": kind = UgxEasingKind.EaseOut; return true; case "ease-in-out": kind = UgxEasingKind.EaseInOut; return true; case "linear": kind = UgxEasingKind.Linear; return true; case "ease-in-sine": kind = UgxEasingKind.EaseInSine; return true; case "ease-out-sine": kind = UgxEasingKind.EaseOutSine; return true; case "ease-in-out-sine": kind = UgxEasingKind.EaseInOutSine; return true; case "ease-in-cubic": kind = UgxEasingKind.EaseInCubic; return true; case "ease-out-cubic": kind = UgxEasingKind.EaseOutCubic; return true; case "ease-in-out-cubic": kind = UgxEasingKind.EaseInOutCubic; return true; case "ease-in-circ": kind = UgxEasingKind.EaseInCirc; return true; case "ease-out-circ": kind = UgxEasingKind.EaseOutCirc; return true; case "ease-in-out-circ": kind = UgxEasingKind.EaseInOutCirc; return true; case "ease-in-elastic": kind = UgxEasingKind.EaseInElastic; return true; case "ease-out-elastic": kind = UgxEasingKind.EaseOutElastic; return true; case "ease-in-out-elastic": kind = UgxEasingKind.EaseInOutElastic; return true; case "ease-in-back": kind = UgxEasingKind.EaseInBack; return true; case "ease-out-back": kind = UgxEasingKind.EaseOutBack; return true; case "ease-in-out-back": kind = UgxEasingKind.EaseInOutBack; return true; case "ease-in-bounce": kind = UgxEasingKind.EaseInBounce; return true; case "ease-out-bounce": kind = UgxEasingKind.EaseOutBounce; return true; case "ease-in-out-bounce": kind = UgxEasingKind.EaseInOutBounce; return true; default: kind = UgxEasingKind.Ease; return false; } } public static float Evaluate(UgxEasingKind kind, float t) { if (t <= 0f) return 0f; if (t >= 1f) return 1f; switch (kind) { case UgxEasingKind.Linear: return t; case UgxEasingKind.Ease: case UgxEasingKind.EaseInOut: return InOutQuad(t); case UgxEasingKind.EaseIn: return t * t; case UgxEasingKind.EaseOut: return t * (2f - t); case UgxEasingKind.EaseInSine: return 1f - Mathf.Cos(t * Mathf.PI * 0.5f); case UgxEasingKind.EaseOutSine: return Mathf.Sin(t * Mathf.PI * 0.5f); case UgxEasingKind.EaseInOutSine: return -(Mathf.Cos(Mathf.PI * t) - 1f) * 0.5f; case UgxEasingKind.EaseInCubic: return t * t * t; case UgxEasingKind.EaseOutCubic: return 1f - Cube(1f - t); case UgxEasingKind.EaseInOutCubic: return t < 0.5f ? 4f * t * t * t : 1f - Cube(-2f * t + 2f) * 0.5f; case UgxEasingKind.EaseInCirc: return 1f - Mathf.Sqrt(Mathf.Max(0f, 1f - t * t)); case UgxEasingKind.EaseOutCirc: return Mathf.Sqrt(Mathf.Max(0f, 1f - Sqr(t - 1f))); case UgxEasingKind.EaseInOutCirc: return t < 0.5f ? (1f - Mathf.Sqrt(Mathf.Max(0f, 1f - Sqr(2f * t)))) * 0.5f : (Mathf.Sqrt(Mathf.Max(0f, 1f - Sqr(-2f * t + 2f))) + 1f) * 0.5f; case UgxEasingKind.EaseInElastic: return InElastic(t); case UgxEasingKind.EaseOutElastic: return OutElastic(t); case UgxEasingKind.EaseInOutElastic: return t < 0.5f ? InElastic(t * 2f) * 0.5f : OutElastic(t * 2f - 1f) * 0.5f + 0.5f; case UgxEasingKind.EaseInBack: return t * t * ((BackConstant + 1f) * t - BackConstant); case UgxEasingKind.EaseOutBack: return 1f - InBack(1f - t); case UgxEasingKind.EaseInOutBack: return t < 0.5f ? InBack(t * 2f) * 0.5f : (1f - InBack(2f - t * 2f)) * 0.5f + 0.5f; case UgxEasingKind.EaseInBounce: return 1f - OutBounce(1f - t); case UgxEasingKind.EaseOutBounce: return OutBounce(t); case UgxEasingKind.EaseInOutBounce: return t < 0.5f ? (1f - OutBounce(1f - t * 2f)) * 0.5f : OutBounce(t * 2f - 1f) * 0.5f + 0.5f; default: return t; } } /// /// CSS cubic-bezier(x1, y1, x2, y2). Newton iterations followed by bisection, the same /// scheme browsers use; costs about 12 multiplies and allocates nothing. /// public static float Bezier(float t, float x1, float y1, float x2, float y2) { if (t <= 0f) return 0f; if (t >= 1f) return 1f; float u = t; for (int i = 0; i < 8; i++) { float x = BezierAxis(u, x1, x2) - t; if (Mathf.Abs(x) < 1e-5f) return BezierAxis(u, y1, y2); float d = BezierSlope(u, x1, x2); if (Mathf.Abs(d) < 1e-6f) break; u -= x / d; } float lo = 0f, hi = 1f; u = t; for (int i = 0; i < 20; i++) { float x = BezierAxis(u, x1, x2); if (Mathf.Abs(x - t) < 1e-5f) break; if (x > t) hi = u; else lo = u; u = (lo + hi) * 0.5f; } return BezierAxis(u, y1, y2); } static float BezierAxis(float u, float a, float b) { float inv = 1f - u; return 3f * inv * inv * u * a + 3f * inv * u * u * b + u * u * u; } static float BezierSlope(float u, float a, float b) { float inv = 1f - u; return 3f * inv * inv * a + 6f * inv * u * (b - a) + 3f * u * u * (1f - b); } static float InOutQuad(float t) => t < 0.5f ? 2f * t * t : 1f - Sqr(-2f * t + 2f) * 0.5f; static float InBack(float t) => t * t * ((BackConstant + 1f) * t - BackConstant); static float InElastic(float t) => 1f - OutElastic(1f - t); static float OutElastic(float t) { const float twoPiOverPeriod = 2f * Mathf.PI / ElasticPeriod; return Mathf.Pow(2f, -10f * t) * Mathf.Sin((t - ElasticPeriod * 0.25f) * twoPiOverPeriod) + 1f; } static float OutBounce(float t) { const float n = 7.5625f; const float d = 2.75f; if (t < 1f / d) return n * t * t; if (t < 2f / d) { t -= 1.5f / d; return n * t * t + 0.75f; } if (t < 2.5f / d) { t -= 2.25f / d; return n * t * t + 0.9375f; } t -= 2.625f / d; return n * t * t + 0.984375f; } static float Sqr(float v) => v * v; static float Cube(float v) => v * v * v; } }