reflex/reflex/experimental/bezier.py

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