using System;
namespace Cthangover.Live2D.Cubism.Framework
{
///
/// Math utilities shared across the Cubism animation pipeline.
/// Includes easing functions (sine), angle utilities, linear
/// interpolation helpers, and bezier curve evaluators with both
/// analytic (Cardano) and binary search approaches.
///
/// The bezier methods handle Cubism's segmented animation curves:
/// calculates a value at a given time,
/// solves for the exact
/// bezier parameter t, and
/// is a fallback when beziers are restricted (non-standard).
///
public static class CubismMath
{
///
/// Sine-based easing function mapping [0,1] -> [0,1] with smooth
/// acceleration and deceleration. Used for fade-in/fade-out curves
/// in motion transitions.
/// 0.5 - 0.5 * cos(pi * t), clamped to [0,1].
///
public static float GetEasingSine(float t)
{
if (t < 0f) return 0f;
if (t > 1f) return 1f;
return 0.5f - 0.5f * MathF.Cos(MathF.PI * t);
}
/// Converts degrees to radians.
public static float DegreesToRadian(float degrees)
{
return degrees * (MathF.PI / 180f);
}
/// Converts radians to degrees.
public static float RadianToDegrees(float radian)
{
return radian * (180f / MathF.PI);
}
///
/// Computes the signed angle in radians from vector (fromX,fromY) to vector (toX,toY).
/// Uses Atan2(det, dot) for the angle between two vectors.
///
public static float DirectionToRadian(float fromX, float fromY, float toX, float toY)
{
var dot = fromX * toX + fromY * toY;
var det = fromX * toY - fromY * toX;
return MathF.Atan2(det, dot);
}
///
/// Converts a radian angle to a unit direction vector (sin, cos).
///
public static void RadianToDirection(float radian, out float x, out float y)
{
x = MathF.Sin(radian);
y = MathF.Cos(radian);
}
/// Standard linear interpolation between two float values.
public static float LinearEvaluation(float startValue, float endValue, float time)
{
return startValue + (endValue - startValue) * time;
}
///
/// Linear interpolation between two values,
/// using their stored Time properties to compute the interpolation factor.
///
public static float LinearEvaluation(ref CubismMotionPoint p0, ref CubismMotionPoint p1, float time)
{
var t = (time - p0.Time) / (p1.Time - p0.Time);
return p0.Value + (p1.Value - p0.Value) * t;
}
///
/// De Casteljau cubic bezier evaluation.
/// Given 4 control points (cp0..cp3), each with a time and value,
/// returns the bezier value at the specified time.
///
public static float BezierEvaluate(
float cp0Time, float cp0Value,
float cp1Time, float cp1Value,
float cp2Time, float cp2Value,
float cp3Time, float cp3Value,
float time)
{
var t = (time - cp0Time) / (cp3Time - cp0Time);
var t1 = 1f - t;
var t2 = t * t;
var t12 = t1 * t1;
var value =
t12 * t1 * cp0Value +
3f * t12 * t * cp1Value +
3f * t1 * t2 * cp2Value +
t2 * t * cp3Value;
return value;
}
///
/// Cardano cubic formula to solve for the exact bezier parameter t
/// given a target time. Used when beziers are not restricted —
/// the analytic solution is faster than binary search.
///
/// Returns the t value that, when applied to the bezier equation,
/// produces the targetTime (or very close to it).
///
public static float CardanoAlgorithmForBezier(
float cp0Time, float cp1Time, float cp2Time, float cp3Time,
float targetTime)
{
var dx = cp0Time - targetTime;
var cx = cp1Time - cp0Time;
var bx = cp2Time - cp1Time;
var ax = cp3Time - cp2Time;
var a2 = ax - cx;
var ab = 2f * (ax - 2f * bx + cx);
var a0 = 3f * (bx - cx);
var ad = -cx;
var a = a2 / dx;
var b = ab / dx;
var c = a0 / dx;
var d = ad / dx;
var r = (-2f * b * b * b + 9f * a * b * c - 27f * a * a * d) / (54f * a * a * a);
var q = (b * b - 3f * a * c) / (9f * a * a);
var q3 = q * q * q;
var floorTerm = r * r - q3;
if (MathF.Abs(floorTerm) < 1e-7f) floorTerm = 0f;
if (floorTerm < 0f)
{
var theta = MathF.Acos(r / MathF.Sqrt(q3));
var sqrtQ = MathF.Sqrt(q);
return -2f * sqrtQ * MathF.Cos(theta / 3f) - b / (3f * a);
}
var sqrtTerm = MathF.Sqrt(floorTerm);
var aTerm = MathF.Cbrt(MathF.Abs(r + sqrtTerm));
var bTerm = r > 0f
? MathF.Cbrt(MathF.Abs(r - sqrtTerm))
: -MathF.Cbrt(MathF.Abs(r - sqrtTerm));
return aTerm - bTerm - b / (3f * a);
}
///
/// Binary search bezier evaluation — fallback for when
/// is true.
/// Iteratively narrows t in [0,1] until the bezier time at t
/// is within epsilon of the target time, then evaluates the value.
/// Capped at 20 iterations with 1e-5 tolerance.
///
public static float BezierEvaluateBinarySearch(
float cp0Time, float cp1Time, float cp2Time, float cp3Time,
float cp0Value, float cp1Value, float cp2Value, float cp3Value,
float targetTime)
{
const int maxIterations = 20;
const float epsilon = 1e-5f;
var tMin = 0f;
var tMax = 1f;
for (var i = 0; i < maxIterations; i++)
{
var t = (tMin + tMax) * 0.5f;
var t1 = 1f - t;
var t2 = t * t;
var t12 = t1 * t1;
var timeAtT =
t12 * t1 * cp0Time +
3f * t12 * t * cp1Time +
3f * t1 * t2 * cp2Time +
t2 * t * cp3Time;
if (MathF.Abs(timeAtT - targetTime) < epsilon)
return
t12 * t1 * cp0Value +
3f * t12 * t * cp1Value +
3f * t1 * t2 * cp2Value +
t2 * t * cp3Value;
if (timeAtT < targetTime)
tMin = t;
else
tMax = t;
}
var tf = (tMin + tMax) * 0.5f;
var tf1 = 1f - tf;
var tf2 = tf * tf;
var tf12 = tf1 * tf1;
return
tf12 * tf1 * cp0Value +
3f * tf12 * tf * cp1Value +
3f * tf1 * tf2 * cp2Value +
tf2 * tf * cp3Value;
}
}
}