using System; using System.Collections.Generic; namespace T3.Core.Animation; internal static class SplineInterpolator { internal static void UpdateTangents(SortedList table) { var count = table.Count; if (count <= 1) return; var keys = table.Keys; var values = table.Values; // First key: start tangent var curKey = keys[0]; var curDef = values[0]; var nextKey = keys[1]; var nextDef = values[1]; curDef.OutTangentAngle = CalcStartTangent(curKey, curDef, nextKey, nextDef); curDef.InTangentAngle = curDef.OutTangentAngle - Math.PI; // Middle keys for (int i = 1; i < count - 1; ++i) { var prevKey = keys[i - 1]; var prevDef = values[i - 1]; curKey = keys[i]; curDef = values[i]; nextKey = keys[i + 1]; nextDef = values[i + 1]; if (NeedsTangentComputation(curDef)) { curDef.InTangentAngle = CalcInTangent(prevKey, prevDef, curKey, curDef, nextKey, nextDef); curDef.OutTangentAngle = CalcOutTangent(prevKey, prevDef, curKey, curDef, nextKey, nextDef); } } // Last key: end tangent var prevLastKey = keys[count - 2]; var prevLastDef = values[count - 2]; var lastKey = keys[count - 1]; var lastDef = values[count - 1]; lastDef.InTangentAngle = CalcEndTangent(prevLastKey, prevLastDef, lastKey, lastDef); lastDef.OutTangentAngle = lastDef.InTangentAngle - Math.PI; } private static bool NeedsTangentComputation(VDefinition def) { return def.InInterpolation != VDefinition.KeyInterpolation.Constant || def.OutInterpolation != VDefinition.KeyInterpolation.Constant; } /// /// Cubic Hermite spline interpolation between two keys. /// See http://en.wikipedia.org/wiki/Monotone_cubic_interpolation /// public static double Interpolate(KeyValuePair a, KeyValuePair b, double u) { double t = (u - a.Key) / (b.Key - a.Key); double tangentLength = b.Key - a.Key; var p0 = a.Value.Value; var m0 = SlopFromAngle(a.Value.OutTangentAngle) * tangentLength * a.Value.TensionOut; var p1 = b.Value.Value; var m1 = SlopFromAngle(b.Value.InTangentAngle) * tangentLength * b.Value.TensionIn; var t2 = t * t; var t3 = t2 * t; return (2 * t3 - 3 * t2 + 1) * p0 + (t3 - 2 * t2 + t) * m0 + (-2 * t3 + 3 * t2) * p1 + (t3 - t2) * m1; } /// /// Computes tan(angle) with precision guard: snaps near-zero results to exactly 0. /// Avoids floating-point noise from tan(PI) ≈ -1.22e-16 which causes visible /// wobble on flat curves when amplified by tangent length and min/max normalization. /// private static double SlopFromAngle(double angle) { var slope = Math.Tan(angle); return Math.Abs(slope) < 1e-10 ? 0.0 : slope; } private static double CalcStartTangent(double aKey, VDefinition aDef, double bKey, VDefinition bDef) { switch (aDef.OutInterpolation) { case VDefinition.KeyInterpolation.Tangent: return aDef.OutTangentAngle; case VDefinition.KeyInterpolation.Linear: case VDefinition.KeyInterpolation.Smooth: case VDefinition.KeyInterpolation.Cubic: return Math.PI / 2 - Math.Atan2(aKey - bKey, aDef.Value - bDef.Value); case VDefinition.KeyInterpolation.Horizontal: default: return Math.PI; } } private static double CalcEndTangent(double aKey, VDefinition aDef, double bKey, VDefinition bDef) { switch (bDef.InInterpolation) { case VDefinition.KeyInterpolation.Tangent: return bDef.InTangentAngle; case VDefinition.KeyInterpolation.Linear: case VDefinition.KeyInterpolation.Smooth: case VDefinition.KeyInterpolation.Cubic: return Math.PI / 2 - Math.Atan2(bKey - aKey, bDef.Value - aDef.Value); case VDefinition.KeyInterpolation.Horizontal: default: return 0; } } private const double TANGENT_CLAMP_RATIO = 1.5; private static double CalcInTangent(double prevKey, VDefinition prevDef, double curKey, VDefinition curDef, double nextKey, VDefinition nextDef) { switch (curDef.InInterpolation) { case VDefinition.KeyInterpolation.Tangent: return curDef.InTangentAngle; case VDefinition.KeyInterpolation.Smooth: var angle = Math.PI / 2 - Math.Atan2(nextKey - prevKey, nextDef.Value - prevDef.Value); double thirdToPrev = (prevKey - curKey) / TANGENT_CLAMP_RATIO; double thirdToNext = (nextKey - curKey) / TANGENT_CLAMP_RATIO; // Avoid overshooting toward next keyframe if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) < nextDef.Value) { angle = Math.PI + Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Max(0, curDef.Value - nextDef.Value)); } else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) > nextDef.Value) { angle = Math.PI + Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Min(0, curDef.Value - nextDef.Value)); } // Avoid overshooting toward previous keyframe else if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) > prevDef.Value) { angle = Math.PI + Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Max(0, -curDef.Value + prevDef.Value)); } else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) < prevDef.Value) { angle = Math.PI + Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Min(0, -curDef.Value + prevDef.Value)); } return angle; case VDefinition.KeyInterpolation.Cubic: return Math.PI / 2 - Math.Atan2(nextKey - prevKey, nextDef.Value - prevDef.Value); case VDefinition.KeyInterpolation.Linear: return Math.PI / 2 - Math.Atan2(curKey - prevKey, curDef.Value - prevDef.Value); case VDefinition.KeyInterpolation.Horizontal: default: return 0; } } private static double CalcOutTangent(double prevKey, VDefinition prevDef, double curKey, VDefinition curDef, double nextKey, VDefinition nextDef) { switch (curDef.OutInterpolation) { case VDefinition.KeyInterpolation.Tangent: return curDef.OutTangentAngle; case VDefinition.KeyInterpolation.Smooth: double thirdToNext = (nextKey - curKey) / TANGENT_CLAMP_RATIO; double thirdToPrev = (prevKey - curKey) / TANGENT_CLAMP_RATIO; var angle = Math.PI / 2 - Math.Atan2(prevKey - nextKey, prevDef.Value - nextDef.Value); // Avoid overshoot toward next keyframe if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) < nextDef.Value) { angle = Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Max(0, curDef.Value - nextDef.Value)); } else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToNext) > nextDef.Value) { angle = Math.PI / 2 - Math.Atan2(-thirdToNext, Math.Min(0, curDef.Value - nextDef.Value)); } // Avoid overshooting toward prev keyframe else if (prevDef.Value > nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) > prevDef.Value) { angle = Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Max(0, -curDef.Value + prevDef.Value)); } else if (prevDef.Value < nextDef.Value && (curDef.Value + Math.Tan(angle) * thirdToPrev) < prevDef.Value) { angle = Math.PI / 2 - Math.Atan2(thirdToPrev, Math.Min(0, -curDef.Value + prevDef.Value)); } return angle; case VDefinition.KeyInterpolation.Cubic: return Math.PI / 2 - Math.Atan2(prevKey - nextKey, prevDef.Value - nextDef.Value); case VDefinition.KeyInterpolation.Linear: return Math.PI / 2 - Math.Atan2(curKey - nextKey, curDef.Value - nextDef.Value); case VDefinition.KeyInterpolation.Horizontal: default: return Math.PI; } } }