Merge pull request #1379 from friedkeenan/3b1b-animate

Add .animate syntax
This commit is contained in:
Grant Sanderson 2021-02-10 13:43:08 -08:00 committed by GitHub
commit 4ff876b536
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 223 additions and 133 deletions

View file

@ -12,11 +12,11 @@ class SquareToCircle(Scene):
self.play(ReplacementTransform(square, circle))
self.wait()
# Try typing the following lines
# self.play(circle.stretch, 4, {"dim": 0})
# self.play(circle.animate.stretch(4, dim=0))
# self.play(Rotate(circle, TAU / 4))
# self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
# self.play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25))
# circle.insert_n_curves(10)
# self.play(circle.apply_complex_function, lambda z: z**2)
# self.play(circle.animate.apply_complex_function(lambda z: z**2))
class SquareToCircleEmbed(Scene):
def construct(self):
@ -26,12 +26,12 @@ class SquareToCircleEmbed(Scene):
self.add(circle)
self.wait()
self.play(circle.stretch, 4, {"dim": 0})
self.play(circle.animate.stretch(4, dim=0))
self.wait(1.5)
self.play(Rotate(circle, TAU / 4))
self.wait(1.5)
self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
self.play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25))
self.wait(1.5)
circle.insert_n_curves(10)
self.play(circle.apply_complex_function, lambda z: z**2)
self.play(circle.animate.apply_complex_function(lambda z: z**2))
self.wait(2)

View file

@ -33,9 +33,9 @@ InteractiveDevlopment
# the interactive shell
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.stretch, 4, 0)
self.play(circle.animate.stretch(4, 0))
self.play(Rotate(circle, 90 * DEGREES))
self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
self.play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25))
text = Text("""
In general, using the interactive shell
@ -73,43 +73,52 @@ AnimatingMethods
grid = Tex(r"\pi").get_grid(10, 10, height=4)
self.add(grid)
# If you pass in a mobject method to the scene's "play" function,
# it will apply an animation interpolating between the mobject's
# initial state and whatever happens when you apply that method.
# For example, calling grid.shift(2 * LEFT) would shift it two units
# to the left, but the following line animates that motion.
self.play(grid.shift, 2 * LEFT)
# You can animate the application of mobject methods with the
# ".animate" syntax:
self.play(grid.animate.shift(LEFT))
# Alternatively, you can use the older syntax by passing the
# method and then the arguments to the scene's "play" function:
self.play(grid.shift, LEFT)
# Both of those will interpolate between the mobject's initial
# state and whatever happens when you apply that method.
# For this example, calling grid.shift(LEFT) would shift the
# grid one unit to the left, but both of the previous calls to
# "self.play" animate that motion.
# The same applies for any method, including those setting colors.
self.play(grid.set_color, YELLOW)
self.play(grid.animate.set_color(YELLOW))
self.wait()
self.play(grid.set_submobject_colors_by_gradient, BLUE, GREEN)
self.play(grid.animate.set_submobject_colors_by_gradient(BLUE, GREEN))
self.wait()
self.play(grid.set_height, TAU - MED_SMALL_BUFF)
self.play(grid.animate.set_height(TAU - MED_SMALL_BUFF))
self.wait()
# The method Mobject.apply_complex_function lets you apply arbitrary
# complex functions, treating the points defining the mobject as
# complex numbers.
self.play(grid.apply_complex_function, np.exp, run_time=5)
self.play(grid.animate.apply_complex_function(np.exp), run_time=5)
self.wait()
# Even more generally, you could apply Mobject.apply_function,
# which takes in functions form R^3 to R^3
self.play(
grid.apply_function,
grid.animate.apply_function(
lambda p: [
p[0] + 0.5 * math.sin(p[1]),
p[1] + 0.5 * math.sin(p[0]),
p[2]
],
]
),
run_time=5,
)
self.wait()
The new usage in this scene is ``.get_grid()`` and ``self.play(mob.method, args)``.
The new usage in this scene is ``.get_grid()`` and ``self.play(mob.animate.method(args))``.
- ``.get_grid()`` method will return a new mobject containing multiple copies of this one arranged in a grid.
- ``self.play(mob.method, args)`` animate the method, and the details are in the comments above.
- ``self.play(mob.animate.method(args))`` animates the method, and the details are in the comments above.
TextExample
-----------
@ -328,18 +337,18 @@ UpdatersExample
# Notice that the brace and label track with the square
self.play(
square.scale, 2,
square.animate.scale(2),
rate_func=there_and_back,
run_time=2,
)
self.wait()
self.play(
square.set_width, 5, {"stretch": True},
square.set_width(5, stretch=True),
run_time=3,
)
self.wait()
self.play(
square.set_width, 2,
square.animate.set_width(2),
run_time=3
)
self.wait()
@ -412,9 +421,9 @@ CoordinateSystemExample
dot = Dot(color=RED)
dot.move_to(axes.c2p(0, 0))
self.play(FadeIn(dot, scale=0.5))
self.play(dot.move_to, axes.c2p(3, 2))
self.play(dot.animate.move_to(axes.c2p(3, 2)))
self.wait()
self.play(dot.move_to, axes.c2p(5, 0.5))
self.play(dot.animate.move_to(axes.c2p(5, 0.5)))
self.wait()
# Similarly, you can call axes.point_to_coords, or axes.p2c
@ -431,9 +440,9 @@ CoordinateSystemExample
ShowCreation(h_line),
ShowCreation(v_line),
)
self.play(dot.move_to, axes.c2p(3, -2))
self.play(dot.animate.move_to(axes.c2p(3, -2)))
self.wait()
self.play(dot.move_to, axes.c2p(1, 1))
self.play(dot.animate.move_to(axes.c2p(1, 1)))
self.wait()
# If we tie the dot to a particular set of coordinates, notice
@ -441,8 +450,8 @@ CoordinateSystemExample
# system defined by them.
f_always(dot.move_to, lambda: axes.c2p(1, 1))
self.play(
axes.scale, 0.75,
axes.to_corner, UL,
axes.animate.scale(0.75),
axes.animate.to_corner(UL),
run_time=2,
)
self.wait()
@ -534,8 +543,8 @@ GraphExample
lambda: axes.i2gp(x_tracker.get_value(), parabola)
)
self.play(x_tracker.set_value, 4, run_time=3)
self.play(x_tracker.set_value, -2, run_time=3)
self.play(x_tracker.animate.set_value(4), run_time=3)
self.play(x_tracker.animate.set_value(-2), run_time=3)
self.wait()
SurfaceExample
@ -608,8 +617,8 @@ SurfaceExample
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.increment_phi, -10 * DEGREES,
frame.increment_theta, -20 * DEGREES,
frame.animate.increment_phi(-10 * DEGREES),
frame.animate.increment_theta(-20 * DEGREES),
run_time=3
)
# Add ambient rotation
@ -624,8 +633,8 @@ SurfaceExample
light = self.camera.light_source
self.add(light)
light.save_state()
self.play(light.move_to, 3 * IN, run_time=5)
self.play(light.shift, 10 * OUT, run_time=5)
self.play(light.animate.move_to(3 * IN), run_time=5)
self.play(light.animate.shift(10 * OUT), run_time=5)
drag_text = Text("Try moving the mouse while pressing d or s")
drag_text.move_to(light_text)
@ -675,7 +684,7 @@ OpeningManimExample
FadeTransform(intro_words, linear_transform_words)
)
self.wait()
self.play(grid.apply_matrix, matrix, run_time=3)
self.play(grid.animate.apply_matrix(matrix), run_time=3)
self.wait()
# Complex map
@ -699,7 +708,7 @@ OpeningManimExample
)
self.wait()
self.play(
moving_c_grid.apply_complex_function, lambda z: z**2,
moving_c_grid.animate.apply_complex_function(lambda z: z**2),
run_time=6,
)
self.wait(2)

View file

@ -221,15 +221,15 @@ For example: input the following lines (without comment lines) into it respectiv
.. code-block:: python
# Stretched 4 times in the vertical direction
play(circle.stretch, 4, {"dim": 0})
play(circle.animate.stretch(4, dim=0}))
# Rotate the ellipse 90°
play(Rotate(circle, TAU / 4))
# Move 2 units to the right and shrink to 1/4 of the original
play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25))
# Insert 10 curves into circle for non-linear transformation (no animation will play)
circle.insert_n_curves(10)
# Apply a complex transformation of f(z)=z^2 to all points on the circle
play(circle.apply_complex_function, lambda z: z**2)
play(circle.animate.apply_complex_function(lambda z: z**2))
# Close the window and exit the program
exit()

View file

@ -37,7 +37,7 @@ class OpeningManimExample(Scene):
FadeTransform(intro_words, linear_transform_words)
)
self.wait()
self.play(grid.apply_matrix, matrix, run_time=3)
self.play(grid.animate.apply_matrix(matrix), run_time=3)
self.wait()
# Complex map
@ -61,7 +61,7 @@ class OpeningManimExample(Scene):
)
self.wait()
self.play(
moving_c_grid.apply_complex_function, lambda z: z**2,
moving_c_grid.animate.apply_complex_function(lambda z: z**2),
run_time=6,
)
self.wait(2)
@ -72,35 +72,44 @@ class AnimatingMethods(Scene):
grid = Tex(r"\pi").get_grid(10, 10, height=4)
self.add(grid)
# If you pass in a mobject method to the scene's "play" function,
# it will apply an animation interpolating between the mobject's
# initial state and whatever happens when you apply that method.
# For example, calling grid.shift(2 * LEFT) would shift it two units
# to the left, but the following line animates that motion.
self.play(grid.shift, 2 * LEFT)
# You can animate the application of mobject methods with the
# ".animate" syntax:
self.play(grid.animate.shift(LEFT))
# Alternatively, you can use the older syntax by passing the
# method and then the arguments to the scene's "play" function:
self.play(grid.shift, LEFT)
# Both of those will interpolate between the mobject's initial
# state and whatever happens when you apply that method.
# For this example, calling grid.shift(LEFT) would shift the
# grid one unit to the left, but both of the previous calls to
# "self.play" animate that motion.
# The same applies for any method, including those setting colors.
self.play(grid.set_color, YELLOW)
self.play(grid.animate.set_color(YELLOW))
self.wait()
self.play(grid.set_submobject_colors_by_gradient, BLUE, GREEN)
self.play(grid.animate.set_submobject_colors_by_gradient(BLUE, GREEN))
self.wait()
self.play(grid.set_height, TAU - MED_SMALL_BUFF)
self.play(grid.animate.set_height(TAU - MED_SMALL_BUFF))
self.wait()
# The method Mobject.apply_complex_function lets you apply arbitrary
# complex functions, treating the points defining the mobject as
# complex numbers.
self.play(grid.apply_complex_function, np.exp, run_time=5)
self.play(grid.animate.apply_complex_function(np.exp), run_time=5)
self.wait()
# Even more generally, you could apply Mobject.apply_function,
# which takes in functions form R^3 to R^3
self.play(
grid.apply_function,
grid.animate.apply_function(
lambda p: [
p[0] + 0.5 * math.sin(p[1]),
p[1] + 0.5 * math.sin(p[0]),
p[2]
],
]
),
run_time=5,
)
self.wait()
@ -294,18 +303,18 @@ class UpdatersExample(Scene):
# Notice that the brace and label track with the square
self.play(
square.scale, 2,
square.animate.scale(2),
rate_func=there_and_back,
run_time=2,
)
self.wait()
self.play(
square.set_width, 5, {"stretch": True},
square.set_width(5, stretch=True),
run_time=3,
)
self.wait()
self.play(
square.set_width, 2,
square.animate.set_width(2),
run_time=3
)
self.wait()
@ -361,9 +370,9 @@ class CoordinateSystemExample(Scene):
dot = Dot(color=RED)
dot.move_to(axes.c2p(0, 0))
self.play(FadeIn(dot, scale=0.5))
self.play(dot.move_to, axes.c2p(3, 2))
self.play(dot.animate.move_to(axes.c2p(3, 2)))
self.wait()
self.play(dot.move_to, axes.c2p(5, 0.5))
self.play(dot.animate.move_to(axes.c2p(5, 0.5)))
self.wait()
# Similarly, you can call axes.point_to_coords, or axes.p2c
@ -380,9 +389,9 @@ class CoordinateSystemExample(Scene):
ShowCreation(h_line),
ShowCreation(v_line),
)
self.play(dot.move_to, axes.c2p(3, -2))
self.play(dot.animate.move_to(axes.c2p(3, -2)))
self.wait()
self.play(dot.move_to, axes.c2p(1, 1))
self.play(dot.animate.move_to(axes.c2p(1, 1)))
self.wait()
# If we tie the dot to a particular set of coordinates, notice
@ -390,8 +399,8 @@ class CoordinateSystemExample(Scene):
# system defined by them.
f_always(dot.move_to, lambda: axes.c2p(1, 1))
self.play(
axes.scale, 0.75,
axes.to_corner, UL,
axes.animate.scale(0.75),
axes.animate.to_corner(UL),
run_time=2,
)
self.wait()
@ -477,8 +486,8 @@ class GraphExample(Scene):
lambda: axes.i2gp(x_tracker.get_value(), parabola)
)
self.play(x_tracker.set_value, 4, run_time=3)
self.play(x_tracker.set_value, -2, run_time=3)
self.play(x_tracker.animate.set_value(4), run_time=3)
self.play(x_tracker.animate.set_value(-2), run_time=3)
self.wait()
@ -546,8 +555,8 @@ class SurfaceExample(Scene):
self.play(
Transform(surface, surfaces[2]),
# Move camera frame during the transition
frame.increment_phi, -10 * DEGREES,
frame.increment_theta, -20 * DEGREES,
frame.animate.increment_phi(-10 * DEGREES),
frame.animate.increment_theta(-20 * DEGREES),
run_time=3
)
# Add ambient rotation
@ -562,8 +571,8 @@ class SurfaceExample(Scene):
light = self.camera.light_source
self.add(light)
light.save_state()
self.play(light.move_to, 3 * IN, run_time=5)
self.play(light.shift, 10 * OUT, run_time=5)
self.play(light.animate.move_to(3 * IN), run_time=5)
self.play(light.animate.shift(10 * OUT), run_time=5)
drag_text = Text("Try moving the mouse while pressing d or s")
drag_text.move_to(light_text)
@ -593,9 +602,9 @@ class InteractiveDevlopment(Scene):
# the interactive shell
self.play(ReplacementTransform(square, circle))
self.wait()
self.play(circle.stretch, 4, 0)
self.play(circle.animate.stretch(4, 0))
self.play(Rotate(circle, 90 * DEGREES))
self.play(circle.shift, 2 * RIGHT, circle.scale, 0.25)
self.play(circle.animate.shift(2 * RIGHT), circle.animate.scale(0.25))
text = Text("""
In general, using the interactive shell

View file

@ -1,6 +1,6 @@
from copy import deepcopy
from manimlib.mobject.mobject import Mobject
from manimlib.mobject.mobject import Mobject, _AnimationBuilder
from manimlib.utils.config_ops import digest_config
from manimlib.utils.rate_functions import smooth
from manimlib.utils.simple_functions import clip
@ -159,3 +159,13 @@ class Animation(object):
def is_remover(self):
return self.remover
def prepare_animation(anim):
if isinstance(anim, _AnimationBuilder):
return anim.build()
if isinstance(anim, Animation):
return anim
raise TypeError(f"Object {anim} cannot be converted to an animation")

View file

@ -1,6 +1,6 @@
import numpy as np
from manimlib.animation.animation import Animation
from manimlib.animation.animation import Animation, prepare_animation
from manimlib.mobject.mobject import Group
from manimlib.utils.bezier import integer_interpolate
from manimlib.utils.bezier import interpolate
@ -29,7 +29,7 @@ class AnimationGroup(Animation):
def __init__(self, *animations, **kwargs):
digest_config(self, kwargs)
self.animations = animations
self.animations = [prepare_animation(anim) for anim in animations]
if self.group is None:
self.group = Group(*remove_list_redundancies(
[anim.mobject for anim in animations]

View file

@ -149,6 +149,12 @@ class MoveToTarget(Transform):
)
class _MethodAnimation(MoveToTarget):
def __init__(self, mobject, methods):
self.methods = methods
super().__init__(mobject)
class ApplyMethod(Transform):
def __init__(self, method, *args, **kwargs):
"""

View file

@ -80,6 +80,11 @@ class Mobject(object):
if self.depth_test:
self.apply_depth_test()
@property
def animate(self):
# Borrowed from https://github.com/ManimCommunity/manim/
return _AnimationBuilder(self)
def __str__(self):
return self.__class__.__name__
@ -1573,3 +1578,51 @@ class Point(Mobject):
def set_location(self, new_loc):
self.set_points(np.array(new_loc, ndmin=2, dtype=float))
class _AnimationBuilder:
def __init__(self, mobject):
self.mobject = mobject
self.overridden_animation = None
self.mobject.generate_target()
self.is_chaining = False
self.methods = []
def __getattr__(self, method_name):
method = getattr(self.mobject.target, method_name)
self.methods.append(method)
has_overridden_animation = hasattr(method, "_override_animate")
if (self.is_chaining and has_overridden_animation) or self.overridden_animation:
raise NotImplementedError(
"Method chaining is currently not supported for "
"overridden animations"
)
def update_target(*method_args, **method_kwargs):
if has_overridden_animation:
self.overridden_animation = method._override_animate(
self.mobject, *method_args, **method_kwargs
)
else:
method(*method_args, **method_kwargs)
return self
self.is_chaining = True
return update_target
def build(self):
from manimlib.animation.transform import _MethodAnimation
if self.overridden_animation:
return self.overridden_animation
return _MethodAnimation(self.mobject, self.methods)
def override_animate(method):
def decorator(animation_method):
method._override_animate = animation_method
return animation_method
return decorator

View file

@ -10,7 +10,7 @@ import numpy as np
import time
from IPython.terminal.embed import InteractiveShellEmbed
from manimlib.animation.animation import Animation
from manimlib.animation.animation import prepare_animation
from manimlib.animation.transform import MoveToTarget
from manimlib.mobject.mobject import Point
from manimlib.camera.camera import Camera
@ -348,10 +348,7 @@ class Scene(object):
state["method_args"] = []
for arg in args:
if isinstance(arg, Animation):
compile_method(state)
animations.append(arg)
elif inspect.ismethod(arg):
if inspect.ismethod(arg):
compile_method(state)
state["curr_method"] = arg
elif state["curr_method"] is not None:
@ -362,7 +359,13 @@ class Scene(object):
you meant to pass in as a Scene.play argument
""")
else:
raise Exception("Invalid play arguments")
try:
anim = prepare_animation(arg)
except TypeError:
raise TypeError(f"Unexpected argument {arg} passed to Scene.play()")
compile_method(state)
animations.append(anim)
compile_method(state)
for animation in animations: