"""A selection of rate functions, i.e., *speed curves* for animations.
Please find a standard list at https://easings.net/. Here is a picture
for the non-standard ones
.. manim:: RateFuncExample
:save_last_frame:
class RateFuncExample(Scene):
def construct(self):
x = VGroup()
for k, v in rate_functions.__dict__.items():
if "function" in str(v):
if (
not k.startswith("__")
and not k.startswith("sqrt")
and not k.startswith("bezier")
):
try:
rate_func = v
plot = (
ParametricFunction(
lambda x: [x, rate_func(x), 0],
t_range=[0, 1, .01],
use_smoothing=False,
color=YELLOW,
)
.stretch_to_fit_width(1.5)
.stretch_to_fit_height(1)
)
plot_bg = SurroundingRectangle(plot).set_color(WHITE)
plot_title = (
Text(rate_func.__name__, weight=BOLD)
.scale(0.5)
.next_to(plot_bg, UP, buff=0.1)
)
x.add(VGroup(plot_bg, plot, plot_title))
except: # because functions `not_quite_there`, `function squish_rate_func` are not working.
pass
x.arrange_in_grid(cols=8)
x.height = config.frame_height
x.width = config.frame_width
x.move_to(ORIGIN).scale(0.95)
self.add(x)
There are primarily 3 kinds of standard easing functions:
#. Ease In - The animation has a smooth start.
#. Ease Out - The animation has a smooth end.
#. Ease In Out - The animation has a smooth start as well as smooth end.
.. note:: The standard functions are not exported, so to use them you do something like this:
rate_func=rate_functions.ease_in_sine
On the other hand, the non-standard functions, which are used more commonly, are exported and can be used directly.
.. manim:: RateFunctions1Example
class RateFunctions1Example(Scene):
def construct(self):
line1 = Line(3*LEFT, 3*RIGHT).shift(UP).set_color(RED)
line2 = Line(3*LEFT, 3*RIGHT).set_color(GREEN)
line3 = Line(3*LEFT, 3*RIGHT).shift(DOWN).set_color(BLUE)
dot1 = Dot().move_to(line1.get_left())
dot2 = Dot().move_to(line2.get_left())
dot3 = Dot().move_to(line3.get_left())
label1 = Tex("Ease In").next_to(line1, RIGHT)
label2 = Tex("Ease out").next_to(line2, RIGHT)
label3 = Tex("Ease In Out").next_to(line3, RIGHT)
self.play(
FadeIn(VGroup(line1, line2, line3)),
FadeIn(VGroup(dot1, dot2, dot3)),
Write(VGroup(label1, label2, label3)),
)
self.play(
MoveAlongPath(dot1, line1, rate_func=rate_functions.ease_in_sine),
MoveAlongPath(dot2, line2, rate_func=rate_functions.ease_out_sine),
MoveAlongPath(dot3, line3, rate_func=rate_functions.ease_in_out_sine),
run_time=7
)
self.wait()
"""
from __future__ import annotations
__all__ = [
"linear",
"smooth",
"smoothstep",
"smootherstep",
"smoothererstep",
"rush_into",
"rush_from",
"slow_into",
"double_smooth",
"there_and_back",
"there_and_back_with_pause",
"running_start",
"not_quite_there",
"wiggle",
"squish_rate_func",
"lingering",
"exponential_decay",
]
import typing
from functools import wraps
from math import sqrt
import numpy as np
from ..utils.bezier import bezier
from ..utils.simple_functions import sigmoid
# This is a decorator that makes sure any function it's used on will
# return 0 if t<0 and 1 if t>1.
[docs]
def unit_interval(function):
@wraps(function)
def wrapper(t, *args, **kwargs):
if 0 <= t <= 1:
return function(t, *args, **kwargs)
elif t < 0:
return 0
else:
return 1
return wrapper
# This is a decorator that makes sure any function it's used on will
# return 0 if t<0 or t>1.
[docs]
def zero(function):
@wraps(function)
def wrapper(t, *args, **kwargs):
if 0 <= t <= 1:
return function(t, *args, **kwargs)
else:
return 0
return wrapper
[docs]
@unit_interval
def linear(t: float) -> float:
return t
[docs]
@unit_interval
def smooth(t: float, inflection: float = 10.0) -> float:
error = sigmoid(-inflection / 2)
return min(
max((sigmoid(inflection * (t - 0.5)) - error) / (1 - 2 * error), 0),
1,
)
[docs]
def smoothstep(t: float) -> float:
"""Implementation of the 1st order SmoothStep sigmoid function.
The 1st derivative (speed) is zero at the endpoints.
https://en.wikipedia.org/wiki/Smoothstep
"""
return 0 if t <= 0 else 3 * t**2 - 2 * t**3 if t < 1 else 1
[docs]
def smootherstep(t: float) -> float:
"""Implementation of the 2nd order SmoothStep sigmoid function.
The 1st and 2nd derivatives (speed and acceleration) are zero at the endpoints.
https://en.wikipedia.org/wiki/Smoothstep
"""
return 0 if t <= 0 else 6 * t**5 - 15 * t**4 + 10 * t**3 if t < 1 else 1
[docs]
def smoothererstep(t: float) -> float:
"""Implementation of the 3rd order SmoothStep sigmoid function.
The 1st, 2nd and 3rd derivatives (speed, acceleration and jerk) are zero at the endpoints.
https://en.wikipedia.org/wiki/Smoothstep
"""
alpha = 0
if 0 < t < 1:
alpha = 35 * t**4 - 84 * t**5 + 70 * t**6 - 20 * t**7
elif t >= 1:
alpha = 1
return alpha
[docs]
@unit_interval
def rush_into(t: float, inflection: float = 10.0) -> float:
return 2 * smooth(t / 2.0, inflection)
[docs]
@unit_interval
def rush_from(t: float, inflection: float = 10.0) -> float:
return 2 * smooth(t / 2.0 + 0.5, inflection) - 1
[docs]
@unit_interval
def slow_into(t: float) -> float:
return np.sqrt(1 - (1 - t) * (1 - t))
[docs]
@unit_interval
def double_smooth(t: float) -> float:
if t < 0.5:
return 0.5 * smooth(2 * t)
else:
return 0.5 * (1 + smooth(2 * t - 1))
[docs]
@zero
def there_and_back(t: float, inflection: float = 10.0) -> float:
new_t = 2 * t if t < 0.5 else 2 * (1 - t)
return smooth(new_t, inflection)
[docs]
@zero
def there_and_back_with_pause(t: float, pause_ratio: float = 1.0 / 3) -> float:
a = 1.0 / pause_ratio
if t < 0.5 - pause_ratio / 2:
return smooth(a * t)
elif t < 0.5 + pause_ratio / 2:
return 1
else:
return smooth(a - a * t)
[docs]
@unit_interval
def running_start(
t: float,
pull_factor: float = -0.5,
) -> typing.Iterable: # what is func return type?
return bezier([0, 0, pull_factor, pull_factor, 1, 1, 1])(t)
[docs]
def not_quite_there(
func: typing.Callable[[float], float] = smooth,
proportion: float = 0.7,
) -> typing.Callable[[float], float]:
def result(t):
return proportion * func(t)
return result
[docs]
@zero
def wiggle(t: float, wiggles: float = 2) -> float:
return there_and_back(t) * np.sin(wiggles * np.pi * t)
[docs]
def squish_rate_func(
func: typing.Callable[[float], float],
a: float = 0.4,
b: float = 0.6,
) -> typing.Callable[[float], float]:
def result(t):
if a == b:
return a
if t < a:
return func(0)
elif t > b:
return func(1)
else:
return func((t - a) / (b - a))
return result
# Stylistically, should this take parameters (with default values)?
# Ultimately, the functionality is entirely subsumed by squish_rate_func,
# but it may be useful to have a nice name for with nice default params for
# "lingering", different from squish_rate_func's default params
[docs]
@unit_interval
def lingering(t: float) -> float:
return squish_rate_func(lambda t: t, 0, 0.8)(t)
[docs]
@unit_interval
def exponential_decay(t: float, half_life: float = 0.1) -> float:
# The half-life should be rather small to minimize
# the cut-off error at the end
return 1 - np.exp(-t / half_life)
[docs]
@unit_interval
def ease_in_sine(t: float) -> float:
return 1 - np.cos((t * np.pi) / 2)
[docs]
@unit_interval
def ease_out_sine(t: float) -> float:
return np.sin((t * np.pi) / 2)
[docs]
@unit_interval
def ease_in_out_sine(t: float) -> float:
return -(np.cos(np.pi * t) - 1) / 2
[docs]
@unit_interval
def ease_in_quad(t: float) -> float:
return t * t
[docs]
@unit_interval
def ease_out_quad(t: float) -> float:
return 1 - (1 - t) * (1 - t)
[docs]
@unit_interval
def ease_in_out_quad(t: float) -> float:
return 2 * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 2) / 2
[docs]
@unit_interval
def ease_in_cubic(t: float) -> float:
return t * t * t
[docs]
@unit_interval
def ease_out_cubic(t: float) -> float:
return 1 - pow(1 - t, 3)
[docs]
@unit_interval
def ease_in_out_cubic(t: float) -> float:
return 4 * t * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 3) / 2
[docs]
@unit_interval
def ease_in_quart(t: float) -> float:
return t * t * t * t
[docs]
@unit_interval
def ease_out_quart(t: float) -> float:
return 1 - pow(1 - t, 4)
[docs]
@unit_interval
def ease_in_out_quart(t: float) -> float:
return 8 * t * t * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 4) / 2
[docs]
@unit_interval
def ease_in_quint(t: float) -> float:
return t * t * t * t * t
[docs]
@unit_interval
def ease_out_quint(t: float) -> float:
return 1 - pow(1 - t, 5)
[docs]
@unit_interval
def ease_in_out_quint(t: float) -> float:
return 16 * t * t * t * t * t if t < 0.5 else 1 - pow(-2 * t + 2, 5) / 2
[docs]
@unit_interval
def ease_in_expo(t: float) -> float:
return 0 if t == 0 else pow(2, 10 * t - 10)
[docs]
@unit_interval
def ease_out_expo(t: float) -> float:
return 1 if t == 1 else 1 - pow(2, -10 * t)
[docs]
@unit_interval
def ease_in_out_expo(t: float) -> float:
if t == 0:
return 0
elif t == 1:
return 1
elif t < 0.5:
return pow(2, 20 * t - 10) / 2
else:
return (2 - pow(2, -20 * t + 10)) / 2
[docs]
@unit_interval
def ease_in_circ(t: float) -> float:
return 1 - sqrt(1 - pow(t, 2))
[docs]
@unit_interval
def ease_out_circ(t: float) -> float:
return sqrt(1 - pow(t - 1, 2))
[docs]
@unit_interval
def ease_in_out_circ(t: float) -> float:
return (
(1 - sqrt(1 - pow(2 * t, 2))) / 2
if t < 0.5
else (sqrt(1 - pow(-2 * t + 2, 2)) + 1) / 2
)
[docs]
@unit_interval
def ease_in_back(t: float) -> float:
c1 = 1.70158
c3 = c1 + 1
return c3 * t * t * t - c1 * t * t
[docs]
@unit_interval
def ease_out_back(t: float) -> float:
c1 = 1.70158
c3 = c1 + 1
return 1 + c3 * pow(t - 1, 3) + c1 * pow(t - 1, 2)
[docs]
@unit_interval
def ease_in_out_back(t: float) -> float:
c1 = 1.70158
c2 = c1 * 1.525
return (
(pow(2 * t, 2) * ((c2 + 1) * 2 * t - c2)) / 2
if t < 0.5
else (pow(2 * t - 2, 2) * ((c2 + 1) * (t * 2 - 2) + c2) + 2) / 2
)
[docs]
@unit_interval
def ease_in_elastic(t: float) -> float:
c4 = (2 * np.pi) / 3
if t == 0:
return 0
elif t == 1:
return 1
else:
return -pow(2, 10 * t - 10) * np.sin((t * 10 - 10.75) * c4)
[docs]
@unit_interval
def ease_out_elastic(t: float) -> float:
c4 = (2 * np.pi) / 3
if t == 0:
return 0
elif t == 1:
return 1
else:
return pow(2, -10 * t) * np.sin((t * 10 - 0.75) * c4) + 1
[docs]
@unit_interval
def ease_in_out_elastic(t: float) -> float:
c5 = (2 * np.pi) / 4.5
if t == 0:
return 0
elif t == 1:
return 1
elif t < 0.5:
return -(pow(2, 20 * t - 10) * np.sin((20 * t - 11.125) * c5)) / 2
else:
return (pow(2, -20 * t + 10) * np.sin((20 * t - 11.125) * c5)) / 2 + 1
[docs]
@unit_interval
def ease_in_bounce(t: float) -> float:
return 1 - ease_out_bounce(1 - t)
[docs]
@unit_interval
def ease_out_bounce(t: float) -> float:
n1 = 7.5625
d1 = 2.75
if t < 1 / d1:
return n1 * t * t
elif t < 2 / d1:
return n1 * (t - 1.5 / d1) * (t - 1.5 / d1) + 0.75
elif t < 2.5 / d1:
return n1 * (t - 2.25 / d1) * (t - 2.25 / d1) + 0.9375
else:
return n1 * (t - 2.625 / d1) * (t - 2.625 / d1) + 0.984375
[docs]
@unit_interval
def ease_in_out_bounce(t: float) -> float:
if t < 0.5:
return (1 - ease_out_bounce(1 - 2 * t)) / 2
else:
return (1 + ease_out_bounce(2 * t - 1)) / 2