bezier.py 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. """A module for generating Bezier easing functions."""
  2. # These values are established by empiricism with tests (tradeoff: performance VS precision)
  3. NEWTON_ITERATIONS = 4
  4. NEWTON_MIN_SLOPE = 0.001
  5. SUBDIVISION_PRECISION = 0.0000001
  6. SUBDIVISION_MAX_ITERATIONS = 10
  7. kSplineTableSize = 11
  8. kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
  9. def A(aA1, aA2):
  10. """Calculate A.
  11. Args:
  12. aA1: The first value.
  13. aA2: The second value.
  14. Returns:
  15. The calculated value.
  16. """
  17. return 1.0 - 3.0 * aA2 + 3.0 * aA1
  18. def B(aA1, aA2):
  19. """Calculate B.
  20. Args:
  21. aA1: The first value.
  22. aA2: The second value.
  23. Returns:
  24. The calculated value.
  25. """
  26. return 3.0 * aA2 - 6.0 * aA1
  27. def C(aA1):
  28. """Calculate C.
  29. Args:
  30. aA1: The first value.
  31. Returns:
  32. The calculated value.
  33. """
  34. return 3.0 * aA1
  35. def calcBezier(aT, aA1, aA2):
  36. """Calculate Bezier.
  37. Args:
  38. aT: The time.
  39. aA1: The first value.
  40. aA2: The second value.
  41. Returns:
  42. x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
  43. """
  44. return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT
  45. def getSlope(aT, aA1, aA2):
  46. """Calculate slope.
  47. Args:
  48. aT: The time.
  49. aA1: The first value.
  50. aA2: The second value.
  51. Returns:
  52. dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
  53. """
  54. return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
  55. def binarySubdivide(aX, aA, aB, mX1, mX2):
  56. """Perform a binary subdivide.
  57. Args:
  58. aX: The x value.
  59. aA: The a value.
  60. aB: The b value.
  61. mX1: The x1 value.
  62. mX2: The x2 value.
  63. Returns:
  64. The t value.
  65. """
  66. current_x = aA
  67. current_t = 0
  68. i = 0
  69. while True:
  70. i += 1
  71. if i >= SUBDIVISION_MAX_ITERATIONS:
  72. break
  73. current_t = aA + (aB - aA) / 2.0
  74. current_x = calcBezier(current_t, mX1, mX2) - aX
  75. if current_x > 0.0:
  76. aB = current_t
  77. else:
  78. aA = current_t
  79. if abs(current_x) <= SUBDIVISION_PRECISION:
  80. break
  81. return current_t
  82. def newtonRaphsonIterate(aX, aGuessT, mX1, mX2):
  83. """Perform a Newton-Raphson iteration.
  84. Args:
  85. aX: The x value.
  86. aGuessT: The guess value.
  87. mX1: The x1 value.
  88. mX2: The x2 value.
  89. Returns:
  90. The t value.
  91. """
  92. for _ in range(NEWTON_ITERATIONS):
  93. current_slope = getSlope(aGuessT, mX1, mX2)
  94. if current_slope == 0.0:
  95. return aGuessT
  96. current_x = calcBezier(aGuessT, mX1, mX2) - aX
  97. aGuessT -= current_x / current_slope
  98. return aGuessT
  99. def LinearEasing(x):
  100. """Linear easing function.
  101. Args:
  102. x: The x value.
  103. Returns:
  104. The x value.
  105. """
  106. return x
  107. def bezier(mX1, mY1, mX2, mY2):
  108. """Generate a Bezier easing function.
  109. Args:
  110. mX1: The x1 value.
  111. mY1: The y1 value.
  112. mX2: The x2 value.
  113. mY2: The y2 value.
  114. Raises:
  115. ValueError: If the x values are not in the [0, 1] range.
  116. Returns:
  117. The Bezier easing function.
  118. """
  119. if not (0 <= mX1 <= 1 and 0 <= mX2 <= 1):
  120. raise ValueError("bezier x values must be in [0, 1] range")
  121. if mX1 == mY1 and mX2 == mY2:
  122. return LinearEasing
  123. # Precompute samples table
  124. sampleValues = [
  125. calcBezier(i * kSampleStepSize, mX1, mX2) for i in range(kSplineTableSize)
  126. ]
  127. def getTForX(aX):
  128. intervalStart = 0.0
  129. currentSample = 1
  130. lastSample = kSplineTableSize - 1
  131. while currentSample != lastSample and sampleValues[currentSample] <= aX:
  132. intervalStart += kSampleStepSize
  133. currentSample += 1
  134. currentSample -= 1
  135. # Interpolate to provide an initial guess for t
  136. dist = (aX - sampleValues[currentSample]) / (
  137. sampleValues[currentSample + 1] - sampleValues[currentSample]
  138. )
  139. guessForT = intervalStart + dist * kSampleStepSize
  140. initialSlope = getSlope(guessForT, mX1, mX2)
  141. if initialSlope >= NEWTON_MIN_SLOPE:
  142. return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
  143. elif initialSlope == 0.0:
  144. return guessForT
  145. else:
  146. return binarySubdivide(
  147. aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2
  148. )
  149. def BezierEasing(x):
  150. if x == 0 or x == 1:
  151. return x
  152. return calcBezier(getTForX(x), mY1, mY2)
  153. return BezierEasing