CubicBezier.cs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. // ReSharper disable InconsistentNaming
  2. // Ported from Chromium project https://github.com/chromium/chromium/blob/374d31b7704475fa59f7b2cb836b3b68afdc3d79/ui/gfx/geometry/cubic_bezier.cc
  3. using System;
  4. using Avalonia.Utilities;
  5. // ReSharper disable CompareOfFloatsByEqualityOperator
  6. // ReSharper disable CommentTypo
  7. // ReSharper disable MemberCanBePrivate.Global
  8. // ReSharper disable TooWideLocalVariableScope
  9. // ReSharper disable UnusedMember.Global
  10. #pragma warning disable 649
  11. namespace Avalonia.Animation.Easings
  12. {
  13. /// <summary>
  14. /// Represents a cubic bezier curve and can compute Y coordinate for a given X
  15. /// </summary>
  16. internal unsafe struct CubicBezier
  17. {
  18. const int CUBIC_BEZIER_SPLINE_SAMPLES = 11;
  19. double ax_;
  20. double bx_;
  21. double cx_;
  22. double ay_;
  23. double by_;
  24. double cy_;
  25. double start_gradient_;
  26. double end_gradient_;
  27. double range_min_;
  28. double range_max_;
  29. private bool monotonically_increasing_;
  30. fixed double spline_samples_[CUBIC_BEZIER_SPLINE_SAMPLES];
  31. public CubicBezier(double p1x, double p1y, double p2x, double p2y) : this()
  32. {
  33. InitCoefficients(p1x, p1y, p2x, p2y);
  34. InitGradients(p1x, p1y, p2x, p2y);
  35. InitRange(p1y, p2y);
  36. InitSpline();
  37. }
  38. public readonly double SampleCurveX(double t)
  39. {
  40. // `ax t^3 + bx t^2 + cx t' expanded using Horner's rule.
  41. return ((ax_ * t + bx_) * t + cx_) * t;
  42. }
  43. readonly double SampleCurveY(double t)
  44. {
  45. return ((ay_ * t + by_) * t + cy_) * t;
  46. }
  47. readonly double SampleCurveDerivativeX(double t)
  48. {
  49. return (3.0 * ax_ * t + 2.0 * bx_) * t + cx_;
  50. }
  51. readonly double SampleCurveDerivativeY(double t)
  52. {
  53. return (3.0 * ay_ * t + 2.0 * by_) * t + cy_;
  54. }
  55. public readonly double SolveWithEpsilon(double x, double epsilon)
  56. {
  57. if (x < 0.0)
  58. return 0.0 + start_gradient_ * x;
  59. if (x > 1.0)
  60. return 1.0 + end_gradient_ * (x - 1.0);
  61. return SampleCurveY(SolveCurveX(x, epsilon));
  62. }
  63. void InitCoefficients(double p1x,
  64. double p1y,
  65. double p2x,
  66. double p2y)
  67. {
  68. // Calculate the polynomial coefficients, implicit first and last control
  69. // points are (0,0) and (1,1).
  70. cx_ = 3.0 * p1x;
  71. bx_ = 3.0 * (p2x - p1x) - cx_;
  72. ax_ = 1.0 - cx_ - bx_;
  73. cy_ = 3.0 * p1y;
  74. by_ = 3.0 * (p2y - p1y) - cy_;
  75. ay_ = 1.0 - cy_ - by_;
  76. #if DEBUG
  77. // Bezier curves with x-coordinates outside the range [0,1] for internal
  78. // control points may have multiple values for t for a given value of x.
  79. // In this case, calls to SolveCurveX may produce ambiguous results.
  80. monotonically_increasing_ = p1x >= 0 && p1x <= 1 && p2x >= 0 && p2x <= 1;
  81. #endif
  82. }
  83. void InitGradients(double p1x,
  84. double p1y,
  85. double p2x,
  86. double p2y)
  87. {
  88. // End-point gradients are used to calculate timing function results
  89. // outside the range [0, 1].
  90. //
  91. // There are four possibilities for the gradient at each end:
  92. // (1) the closest control point is not horizontally coincident with regard to
  93. // (0, 0) or (1, 1). In this case the line between the end point and
  94. // the control point is tangent to the bezier at the end point.
  95. // (2) the closest control point is coincident with the end point. In
  96. // this case the line between the end point and the far control
  97. // point is tangent to the bezier at the end point.
  98. // (3) both internal control points are coincident with an endpoint. There
  99. // are two special case that fall into this category:
  100. // CubicBezier(0, 0, 0, 0) and CubicBezier(1, 1, 1, 1). Both are
  101. // equivalent to linear.
  102. // (4) the closest control point is horizontally coincident with the end
  103. // point, but vertically distinct. In this case the gradient at the
  104. // end point is Infinite. However, this causes issues when
  105. // interpolating. As a result, we break down to a simple case of
  106. // 0 gradient under these conditions.
  107. if (p1x > 0)
  108. start_gradient_ = p1y / p1x;
  109. else if (p1y == 0 && p2x > 0)
  110. start_gradient_ = p2y / p2x;
  111. else if (p1y == 0 && p2y == 0)
  112. start_gradient_ = 1;
  113. else
  114. start_gradient_ = 0;
  115. if (p2x < 1)
  116. end_gradient_ = (p2y - 1) / (p2x - 1);
  117. else if (p2y == 1 && p1x < 1)
  118. end_gradient_ = (p1y - 1) / (p1x - 1);
  119. else if (p2y == 1 && p1y == 1)
  120. end_gradient_ = 1;
  121. else
  122. end_gradient_ = 0;
  123. }
  124. const double kBezierEpsilon = 1e-7;
  125. void InitRange(double p1y, double p2y)
  126. {
  127. range_min_ = 0;
  128. range_max_ = 1;
  129. if (0 <= p1y && p1y < 1 && 0 <= p2y && p2y <= 1)
  130. return;
  131. double epsilon = kBezierEpsilon;
  132. // Represent the function's derivative in the form at^2 + bt + c
  133. // as in sampleCurveDerivativeY.
  134. // (Technically this is (dy/dt)*(1/3), which is suitable for finding zeros
  135. // but does not actually give the slope of the curve.)
  136. double a = 3.0 * ay_;
  137. double b = 2.0 * by_;
  138. double c = cy_;
  139. // Check if the derivative is constant.
  140. if (Math.Abs(a) < epsilon && Math.Abs(b) < epsilon)
  141. return;
  142. // Zeros of the function's derivative.
  143. double t1;
  144. double t2 = 0;
  145. if (Math.Abs(a) < epsilon)
  146. {
  147. // The function's derivative is linear.
  148. t1 = -c / b;
  149. }
  150. else
  151. {
  152. // The function's derivative is a quadratic. We find the zeros of this
  153. // quadratic using the quadratic formula.
  154. double discriminant = b * b - 4 * a * c;
  155. if (discriminant < 0)
  156. return;
  157. double discriminant_sqrt = Math.Sqrt(discriminant);
  158. t1 = (-b + discriminant_sqrt) / (2 * a);
  159. t2 = (-b - discriminant_sqrt) / (2 * a);
  160. }
  161. double sol1 = 0;
  162. double sol2 = 0;
  163. // If the solution is in the range [0,1] then we include it, otherwise we
  164. // ignore it.
  165. // An interesting fact about these beziers is that they are only
  166. // actually evaluated in [0,1]. After that we take the tangent at that point
  167. // and linearly project it out.
  168. if (0 < t1 && t1 < 1)
  169. sol1 = SampleCurveY(t1);
  170. if (0 < t2 && t2 < 1)
  171. sol2 = SampleCurveY(t2);
  172. range_min_ = Math.Min(Math.Min(range_min_, sol1), sol2);
  173. range_max_ = Math.Max(Math.Max(range_max_, sol1), sol2);
  174. }
  175. void InitSpline()
  176. {
  177. double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
  178. for (int i = 0; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++)
  179. {
  180. spline_samples_[i] = SampleCurveX(i * delta_t);
  181. }
  182. }
  183. const int kMaxNewtonIterations = 4;
  184. public readonly double SolveCurveX(double x, double epsilon)
  185. {
  186. if (x < 0 || x > 1)
  187. throw new ArgumentException();
  188. double t0 = 0;
  189. double t1 = 0;
  190. double t2 = x;
  191. double x2 = 0;
  192. double d2;
  193. int i;
  194. #if DEBUG
  195. if (!monotonically_increasing_)
  196. throw new InvalidOperationException();
  197. #endif
  198. // Linear interpolation of spline curve for initial guess.
  199. double delta_t = 1.0 / (CUBIC_BEZIER_SPLINE_SAMPLES - 1);
  200. for (i = 1; i < CUBIC_BEZIER_SPLINE_SAMPLES; i++)
  201. {
  202. if (x <= spline_samples_[i])
  203. {
  204. t1 = delta_t * i;
  205. t0 = t1 - delta_t;
  206. t2 = t0 + (t1 - t0) * (x - spline_samples_[i - 1]) /
  207. (spline_samples_[i] - spline_samples_[i - 1]);
  208. break;
  209. }
  210. }
  211. // Perform a few iterations of Newton's method -- normally very fast.
  212. // See https://en.wikipedia.org/wiki/Newton%27s_method.
  213. double newton_epsilon = Math.Min(kBezierEpsilon, epsilon);
  214. for (i = 0; i < kMaxNewtonIterations; i++)
  215. {
  216. x2 = SampleCurveX(t2) - x;
  217. if (Math.Abs(x2) < newton_epsilon)
  218. return t2;
  219. d2 = SampleCurveDerivativeX(t2);
  220. if (Math.Abs(d2) < kBezierEpsilon)
  221. break;
  222. t2 = t2 - x2 / d2;
  223. }
  224. if (Math.Abs(x2) < epsilon)
  225. return t2;
  226. // Fall back to the bisection method for reliability.
  227. while (t0 < t1)
  228. {
  229. x2 = SampleCurveX(t2);
  230. if (Math.Abs(x2 - x) < epsilon)
  231. return t2;
  232. if (x > x2)
  233. t0 = t2;
  234. else
  235. t1 = t2;
  236. t2 = (t1 + t0) * .5;
  237. }
  238. // Failure.
  239. return t2;
  240. }
  241. public readonly double Solve(double x)
  242. {
  243. return SolveWithEpsilon(x, kBezierEpsilon);
  244. }
  245. public readonly double SlopeWithEpsilon(double x, double epsilon)
  246. {
  247. x = MathUtilities.Clamp(x, 0.0, 1.0);
  248. double t = SolveCurveX(x, epsilon);
  249. double dx = SampleCurveDerivativeX(t);
  250. double dy = SampleCurveDerivativeY(t);
  251. return dy / dx;
  252. }
  253. public readonly double Slope(double x)
  254. {
  255. return SlopeWithEpsilon(x, kBezierEpsilon);
  256. }
  257. public readonly double RangeMin => range_min_;
  258. public readonly double RangeMax => range_max_;
  259. }
  260. }