200 lines
4.5 KiB
Python
200 lines
4.5 KiB
Python
"""A module for generating Bezier easing functions."""
|
|
|
|
# These values are established by empiricism with tests (tradeoff: performance VS precision)
|
|
NEWTON_ITERATIONS = 4
|
|
NEWTON_MIN_SLOPE = 0.001
|
|
SUBDIVISION_PRECISION = 0.0000001
|
|
SUBDIVISION_MAX_ITERATIONS = 10
|
|
kSplineTableSize = 11
|
|
kSampleStepSize = 1.0 / (kSplineTableSize - 1.0)
|
|
|
|
|
|
def A(aA1, aA2):
|
|
"""Calculate A.
|
|
|
|
Args:
|
|
aA1: The first value.
|
|
aA2: The second value.
|
|
|
|
Returns:
|
|
The calculated value.
|
|
"""
|
|
return 1.0 - 3.0 * aA2 + 3.0 * aA1
|
|
|
|
|
|
def B(aA1, aA2):
|
|
"""Calculate B.
|
|
|
|
Args:
|
|
aA1: The first value.
|
|
aA2: The second value.
|
|
|
|
Returns:
|
|
The calculated value.
|
|
"""
|
|
return 3.0 * aA2 - 6.0 * aA1
|
|
|
|
|
|
def C(aA1):
|
|
"""Calculate C.
|
|
|
|
Args:
|
|
aA1: The first value.
|
|
|
|
Returns:
|
|
The calculated value.
|
|
"""
|
|
return 3.0 * aA1
|
|
|
|
|
|
def calcBezier(aT, aA1, aA2):
|
|
"""Calculate Bezier.
|
|
|
|
Args:
|
|
aT: The time.
|
|
aA1: The first value.
|
|
aA2: The second value.
|
|
|
|
Returns:
|
|
x(t) given t, x1, and x2, or y(t) given t, y1, and y2.
|
|
"""
|
|
return ((A(aA1, aA2) * aT + B(aA1, aA2)) * aT + C(aA1)) * aT
|
|
|
|
|
|
def getSlope(aT, aA1, aA2):
|
|
"""Calculate slope.
|
|
|
|
Args:
|
|
aT: The time.
|
|
aA1: The first value.
|
|
aA2: The second value.
|
|
|
|
Returns:
|
|
dx/dt given t, x1, and x2, or dy/dt given t, y1, and y2.
|
|
"""
|
|
return 3.0 * A(aA1, aA2) * aT * aT + 2.0 * B(aA1, aA2) * aT + C(aA1)
|
|
|
|
|
|
def binarySubdivide(aX, aA, aB, mX1, mX2):
|
|
"""Perform a binary subdivide.
|
|
|
|
Args:
|
|
aX: The x value.
|
|
aA: The a value.
|
|
aB: The b value.
|
|
mX1: The x1 value.
|
|
mX2: The x2 value.
|
|
|
|
Returns:
|
|
The t value.
|
|
"""
|
|
current_x = aA
|
|
current_t = 0
|
|
i = 0
|
|
while True:
|
|
i += 1
|
|
if i >= SUBDIVISION_MAX_ITERATIONS:
|
|
break
|
|
current_t = aA + (aB - aA) / 2.0
|
|
current_x = calcBezier(current_t, mX1, mX2) - aX
|
|
if current_x > 0.0:
|
|
aB = current_t
|
|
else:
|
|
aA = current_t
|
|
if abs(current_x) <= SUBDIVISION_PRECISION:
|
|
break
|
|
return current_t
|
|
|
|
|
|
def newtonRaphsonIterate(aX, aGuessT, mX1, mX2):
|
|
"""Perform a Newton-Raphson iteration.
|
|
|
|
Args:
|
|
aX: The x value.
|
|
aGuessT: The guess value.
|
|
mX1: The x1 value.
|
|
mX2: The x2 value.
|
|
|
|
Returns:
|
|
The t value.
|
|
"""
|
|
for _ in range(NEWTON_ITERATIONS):
|
|
current_slope = getSlope(aGuessT, mX1, mX2)
|
|
if current_slope == 0.0:
|
|
return aGuessT
|
|
current_x = calcBezier(aGuessT, mX1, mX2) - aX
|
|
aGuessT -= current_x / current_slope
|
|
return aGuessT
|
|
|
|
|
|
def LinearEasing(x):
|
|
"""Linear easing function.
|
|
|
|
Args:
|
|
x: The x value.
|
|
|
|
Returns:
|
|
The x value.
|
|
"""
|
|
return x
|
|
|
|
|
|
def bezier(mX1, mY1, mX2, mY2):
|
|
"""Generate a Bezier easing function.
|
|
|
|
Args:
|
|
mX1: The x1 value.
|
|
mY1: The y1 value.
|
|
mX2: The x2 value.
|
|
mY2: The y2 value.
|
|
|
|
Raises:
|
|
ValueError: If the x values are not in the [0, 1] range.
|
|
|
|
Returns:
|
|
The Bezier easing function.
|
|
"""
|
|
if not (0 <= mX1 <= 1 and 0 <= mX2 <= 1):
|
|
raise ValueError("bezier x values must be in [0, 1] range")
|
|
|
|
if mX1 == mY1 and mX2 == mY2:
|
|
return LinearEasing
|
|
|
|
# Precompute samples table
|
|
sampleValues = [
|
|
calcBezier(i * kSampleStepSize, mX1, mX2) for i in range(kSplineTableSize)
|
|
]
|
|
|
|
def getTForX(aX):
|
|
intervalStart = 0.0
|
|
currentSample = 1
|
|
lastSample = kSplineTableSize - 1
|
|
|
|
while currentSample != lastSample and sampleValues[currentSample] <= aX:
|
|
intervalStart += kSampleStepSize
|
|
currentSample += 1
|
|
currentSample -= 1
|
|
|
|
# Interpolate to provide an initial guess for t
|
|
dist = (aX - sampleValues[currentSample]) / (
|
|
sampleValues[currentSample + 1] - sampleValues[currentSample]
|
|
)
|
|
guessForT = intervalStart + dist * kSampleStepSize
|
|
|
|
initialSlope = getSlope(guessForT, mX1, mX2)
|
|
if initialSlope >= NEWTON_MIN_SLOPE:
|
|
return newtonRaphsonIterate(aX, guessForT, mX1, mX2)
|
|
elif initialSlope == 0.0:
|
|
return guessForT
|
|
else:
|
|
return binarySubdivide(
|
|
aX, intervalStart, intervalStart + kSampleStepSize, mX1, mX2
|
|
)
|
|
|
|
def BezierEasing(x):
|
|
if x == 0 or x == 1:
|
|
return x
|
|
return calcBezier(getTForX(x), mY1, mY2)
|
|
|
|
return BezierEasing
|