Add .animate attribute to mobjects (#881)

Adds a .animate attribute to mobjects to be used to animate methods.
This commit is contained in:
Devin Neal 2020-12-30 05:56:38 -08:00 committed by GitHub
commit ffd8b42d26
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 120 additions and 143 deletions

View file

@ -204,10 +204,10 @@ Animations
def construct(self):
square = Square(color=BLUE, fill_opacity=1)
self.play(square.shift, LEFT)
self.play(square.set_fill, ORANGE)
self.play(square.scale, 0.3)
self.play(square.rotate, 0.4)
self.play(square.animate.shift(LEFT))
self.play(square.animate.set_fill(ORANGE))
self.play(square.animate.scale(0.3))
self.play(square.animate.rotate(0.4))
.. manim:: MovingFrameBox
:ref_modules: manim.mobject.svg.tex_mobject
@ -268,8 +268,8 @@ Animations
self.add(path, dot)
self.play(Rotating(dot, radians=PI, about_point=RIGHT, run_time=2))
self.wait()
self.play(dot.shift, UP)
self.play(dot.shift, LEFT)
self.play(dot.animate.shift(UP))
self.play(dot.animate.shift(LEFT))
self.wait()
@ -394,9 +394,9 @@ Special Camera Settings
moving_dot = Dot().move_to(graph.points[0]).set_color(ORANGE)
dot_at_start_graph = Dot().move_to(graph.points[0])
dot_at_end_grap = Dot().move_to(graph.points[-1])
self.add(graph, dot_at_end_grap, dot_at_start_graph, moving_dot)
self.play( self.camera_frame.scale,0.5,self.camera_frame.move_to,moving_dot)
dot_at_end_graph = Dot().move_to(graph.points[-1])
self.add(graph, dot_at_end_graph, dot_at_start_graph, moving_dot)
self.play(self.camera_frame.animate.scale(0.5),self.camera_frame.animate.move_to(moving_dot))
def update_curve(mob):
mob.move_to(moving_dot.get_center())
@ -462,15 +462,15 @@ Special Camera Settings
# Scale in x y z
scale_factor = [0.5, 1.5, 0]
self.play(
frame.scale, scale_factor,
zoomed_display.scale, scale_factor,
frame.animate.scale(scale_factor),
zoomed_display.animate.scale(scale_factor),
FadeOut(zoomed_camera_text),
FadeOut(frame_text)
)
self.wait()
self.play(ScaleInPlace(zoomed_display, 2))
self.wait()
self.play(frame.shift, 2.5 * DOWN)
self.play(frame.animate.shift(2.5 * DOWN))
self.wait()
self.play(self.get_zoomed_display_pop_out_animation(), unfold_camera, rate_func=lambda t: smooth(1 - t))
self.play(Uncreate(zoomed_display_frame), FadeOut(frame))
@ -640,14 +640,15 @@ Advanced Projects
grid_transform_title.move_to(grid_title, UL)
grid.prepare_for_nonlinear_transform()
self.play(
grid.apply_function,
lambda p: p
+ np.array(
[
np.sin(p[1]),
np.sin(p[0]),
0,
]
grid.animate.apply_function(
lambda p: p
+ np.array(
[
np.sin(p[1]),
np.sin(p[0]),
0,
]
)
),
run_time=3,
)

View file

@ -52,14 +52,15 @@ class OpeningManimExample(Scene):
grid_transform_title.move_to(grid_title, UL)
grid.prepare_for_nonlinear_transform()
self.play(
grid.apply_function,
lambda p: p
+ np.array(
[
np.sin(p[1]),
np.sin(p[0]),
0,
]
grid.animate.apply_function(
lambda p: p
+ np.array(
[
np.sin(p[1]),
np.sin(p[0]),
0,
]
)
),
run_time=3,
)
@ -121,8 +122,7 @@ class UpdatersExample(Scene):
decimal.add_updater(lambda d: d.set_value(square.get_center()[1]))
self.add(square, decimal)
self.play(
square.to_edge,
DOWN,
square.animate.to_edge(DOWN),
rate_func=there_and_back,
run_time=5,
)

View file

@ -128,10 +128,6 @@ class Animation:
def copy(self) -> "Animation":
return deepcopy(self)
def update_config(self, **kwargs: typing.Dict[str, typing.Any]) -> "Animation":
self.__dict__.update(kwargs)
return self
# Methods for interpolation, the mean of an Animation
def interpolate(self, alpha: float) -> None:
alpha = np.clip(alpha, 0, 1)

View file

@ -188,6 +188,11 @@ class MoveToTarget(Transform):
)
class _MethodAnimation(MoveToTarget):
def __init__(self, mobject):
super().__init__(mobject)
class ApplyMethod(Transform):
def __init__(
self, method: types.MethodType, *args, **kwargs

View file

@ -104,7 +104,7 @@ class TracedPath(VMobject):
trace = TracedPath(circ.get_start)
rolling_circle.add_updater(lambda m: m.rotate(-0.3))
self.add(trace, rolling_circle)
self.play(rolling_circle.shift, 8*RIGHT, run_time=4, rate_func=linear)
self.play(rolling_circle.animate.shift(8*RIGHT), run_time=4, rate_func=linear)
"""

View file

@ -57,6 +57,17 @@ class Mobject(Container):
self.init_colors()
Container.__init__(self, **kwargs)
@property
def animate(self):
"""
Used to animate the application of a method.
Examples
--------
self.play(mobject.animate.shift(RIGHT))
"""
return _AnimationBuilder(self)
def __repr__(self):
return str(self.name)
@ -1295,3 +1306,19 @@ class Group(Mobject):
def __init__(self, *mobjects, **kwargs):
Mobject.__init__(self, **kwargs)
self.add(*mobjects)
class _AnimationBuilder:
def __init__(self, mobject):
self.mobject = mobject
def __getattr__(self, method_name):
from ..animation.transform import _MethodAnimation
self.method = getattr(self.mobject.generate_target(), method_name)
def build(*method_args, **method_kwargs):
self.method(*method_args, **method_kwargs)
return _MethodAnimation(self.mobject)
return build

View file

@ -31,8 +31,7 @@ class DecimalNumber(VMobject):
decimal.add_updater(lambda d: d.set_value(square.get_center()[1]))
self.add(square, decimal)
self.play(
square.to_edge,
DOWN,
square.animate.to_edge(DOWN),
rate_func=there_and_back,
run_time=5,
)
@ -261,7 +260,7 @@ class Variable(VMobject):
self.wait()
var_tracker = on_screen_var.tracker
var = 10.5
self.play(var_tracker.set_value, var)
self.play(var_tracker.animate.set_value(var))
self.wait()
int_var = 0
@ -275,7 +274,7 @@ class Variable(VMobject):
self.wait()
var_tracker = on_screen_int_var.tracker
var = 10.5
self.play(var_tracker.set_value, var)
self.play(var_tracker.animate.set_value(var))
self.wait()
# If you wish to have a somewhat more complicated label for your

View file

@ -1063,18 +1063,18 @@ class VGroup(VMobject):
self.wait()
gr += gr2 # Add group to another
self.play(
gr.shift, DOWN,
gr.animate.shift(DOWN),
)
gr -= gr2 # Remove group
self.play( # Animate groups separately
gr.shift, LEFT,
gr2.shift, UP,
gr.animate.shift(LEFT),
gr2.animate.shift(UP),
)
self.play( #Animate groups without modification
(gr+gr2).shift, RIGHT
(gr+gr2).animate.shift(RIGHT)
)
self.play( # Animate group without component
(gr-circle_red).shift, RIGHT
(gr-circle_red).animate.shift(RIGHT)
)
"""
if not all(isinstance(m, VMobject) for m in vmobjects):
@ -1156,7 +1156,7 @@ class VDict(VMobject):
# access submobjects like a python dict
my_dict["t"].set_color(PURPLE)
self.play(my_dict["t"].scale, 3)
self.play(my_dict["t"].animate.scale(3))
self.wait()
# also supports python dict styled reassignment

View file

@ -36,9 +36,9 @@ class ValueTracker(Mobject):
)
)
self.add(number_line, pointer,label)
self.play(pointer_value.set_value, 5)
self.play(pointer_value.animate.set_value(5)),
self.wait()
self.play(pointer_value.set_value, 3)
self.play(pointer_value.animate.set_value(3))
"""

View file

@ -15,7 +15,7 @@ Examples
text = Text("Hello World").set_color(BLUE)
self.add(text)
self.camera_frame.save_state()
self.play(self.camera_frame.set_width, text.get_width() * 1.2)
self.play(self.camera_frame.animate.set_width(text.get_width() * 1.2))
self.wait(0.3)
self.play(Restore(self.camera_frame))
@ -28,9 +28,9 @@ Examples
t = Triangle(color=GREEN, fill_opacity=0.5).move_to(2 * RIGHT)
self.wait(0.3)
self.add(s, t)
self.play(self.camera_frame.move_to, s)
self.play(self.camera_frame.animate.move_to(s))
self.wait(0.3)
self.play(self.camera_frame.move_to, t)
self.play(self.camera_frame.animate.move_to(t))
.. manim:: MovingAndZoomingCamera
@ -40,14 +40,14 @@ Examples
s = Square(color=BLUE, fill_opacity=0.5).move_to(2 * LEFT)
t = Triangle(color=YELLOW, fill_opacity=0.5).move_to(2 * RIGHT)
self.add(s, t)
self.play(self.camera_frame.move_to, s,
self.camera_frame.set_width,s.get_width()*2)
self.play(self.camera_frame.animate.move_to(s),
self.camera_frame.animate.set_width(s.get_width()*2))
self.wait(0.3)
self.play(self.camera_frame.move_to, t,
self.camera_frame.set_width,t.get_width()*2)
self.play(self.camera_frame.animate.move_to(t),
self.camera_frame.animate.set_width(t.get_width()*2))
self.play(self.camera_frame.move_to, ORIGIN,
self.camera_frame.set_width,14)
self.play(self.camera_frame.animate.move_to(ORIGIN),
self.camera_frame.animate.set_width(14))
.. manim:: MovingCameraOnGraph
@ -64,10 +64,10 @@ Examples
x_max=3 * PI
)
dot_at_start_graph = Dot().move_to(graph.points[0])
dot_at_end_grap = Dot().move_to(graph.points[-1])
self.add(graph, dot_at_end_grap, dot_at_start_graph)
self.play(self.camera_frame.scale, 0.5, self.camera_frame.move_to, dot_at_start_graph)
self.play(self.camera_frame.move_to, dot_at_end_grap)
dot_at_end_graph = Dot().move_to(graph.points[-1])
self.add(graph, dot_at_end_graph, dot_at_start_graph)
self.play(self.camera_frame.animate.scale(0.5), self.camera_frame.animate.move_to(dot_at_start_graph))
self.play(self.camera_frame.animate.move_to(dot_at_end_graph))
self.play(Restore(self.camera_frame))
self.wait()

View file

@ -17,11 +17,11 @@ import numpy as np
from .. import config, logger
from ..animation.animation import Animation, Wait
from ..animation.transform import MoveToTarget
from ..animation.transform import MoveToTarget, _MethodAnimation
from ..camera.camera import Camera
from ..constants import *
from ..container import Container
from ..mobject.mobject import Mobject
from ..mobject.mobject import Mobject, _AnimationBuilder
from ..utils.iterables import list_update, list_difference_update
from ..utils.family import extract_mobject_family_members
from ..renderer.cairo_renderer import CairoRenderer
@ -596,83 +596,32 @@ class Scene(Container):
)
return all_moving_mobject_families, static_mobjects
def compile_play_args_to_animation_list(self, *args, **kwargs):
def compile_animations(self, *animations, **play_kwargs):
"""
Each arg can either be an animation, or a mobject method
followed by that methods arguments (and potentially follow
by a dict of kwargs for that method).
This animation list is built by going through the args list,
and each animation is simply added, but when a mobject method
is hit, a MoveToTarget animation is built using the args that
follow up until either another animation is hit, another method
is hit, or the args list runs out.
Creates _MethodAnimations from any _AnimationBuilders and updates animation
kwargs with kwargs passed to play().
Parameters
----------
*args : Animation or method of a mobject, which is followed by that method's arguments
*animations : Tuple[:class:`Animation`]
Animations to be played.
**kwargs : any named arguments like run_time or lag_ratio.
**play_kwargs
Configuration for the call to play().
Returns
-------
list : list of animations with the parameters applied to them.
Tuple[:class:`Animation`]
Animations to be played.
"""
animations = []
state = {
"curr_method": None,
"last_method": None,
"method_args": [],
}
def compile_method(state):
if state["curr_method"] is None:
return
mobject = state["curr_method"].__self__
if state["last_method"] and state["last_method"].__self__ is mobject:
animations.pop()
# method should already have target then.
else:
mobject.generate_target()
#
if len(state["method_args"]) > 0 and isinstance(
state["method_args"][-1], dict
):
method_kwargs = state["method_args"].pop()
else:
method_kwargs = {}
state["curr_method"].__func__(
mobject.target, *state["method_args"], **method_kwargs
)
animations.append(MoveToTarget(mobject))
state["last_method"] = state["curr_method"]
state["curr_method"] = None
state["method_args"] = []
for arg in args:
if isinstance(arg, Animation):
compile_method(state)
animations.append(arg)
elif inspect.ismethod(arg):
compile_method(state)
state["curr_method"] = arg
elif state["curr_method"] is not None:
state["method_args"].append(arg)
elif isinstance(arg, Mobject):
raise ValueError(
"""
I think you may have invoked a method
you meant to pass in as a Scene.play argument
"""
)
else:
raise ValueError("Invalid play arguments")
compile_method(state)
for animation in animations:
# This is where kwargs to play like run_time and rate_func
# get applied to all animations
animation.update_config(**kwargs)
if inspect.ismethod(animation):
raise TypeError(
"Passing mobject methods to Scene.play is no longer supported. Use "
"Mobject.animate instead."
)
for k, v in play_kwargs.items():
setattr(animation, k, v)
return animations
def _get_animation_time_progression(self, animations, duration):
@ -814,15 +763,15 @@ class Scene(Container):
"""
self.wait(max_time, stop_condition=stop_condition)
def compile_animation_data(self, *args, skip_rendering=False, **kwargs):
if len(args) == 0:
def compile_animation_data(self, *animations, skip_rendering=False, **play_kwargs):
if len(animations) == 0:
warnings.warn("Called Scene.play with no animations")
return None
self.last_t = 0
self.animations = self.compile_play_args_to_animation_list(*args, **kwargs)
self.animations = self.compile_animations(*animations, **play_kwargs)
self.add_mobjects_from_animations(self.animations)
self.last_t = 0
self.stop_condition = None
self.moving_mobjects = None
self.static_mobjects = None

View file

@ -13,7 +13,7 @@ Examples
self.wait(1)
self.activate_zooming(animate=False)
self.wait(1)
self.play(dot.shift, LEFT)
self.play(dot.animate.shift(LEFT))
.. manim:: ChangingZoomScale
@ -38,10 +38,10 @@ Examples
self.wait(1)
self.activate_zooming(animate=False)
self.wait(1)
self.play(dot.shift, LEFT * 0.3)
self.play(dot.animate.shift(LEFT * 0.3))
self.play(self.zoomed_camera.frame.scale, 4)
self.play(self.zoomed_camera.frame.shift, 0.5 * DOWN)
self.play(self.zoomed_camera.frame.animate.scale(4))
self.play(self.zoomed_camera.frame.animate.shift(0.5 * DOWN))
"""

View file

@ -20,7 +20,7 @@ def handle_caching_play(func):
def wrapper(self, scene, *args, **kwargs):
self.skip_animations = self.original_skipping_status
self.update_skipping_status()
animations = scene.compile_play_args_to_animation_list(*args, **kwargs)
animations = scene.compile_animations(*args, **kwargs)
scene.add_mobjects_from_animations(animations)
if self.skip_animations:
logger.debug(f"Skipping animation {self.num_plays}")

View file

@ -43,13 +43,13 @@ class RotateTest(Scene):
class MoveToTest(Scene):
def construct(self):
square = Square()
self.play(square.move_to, np.array([1.0, 1.0, 0.0]))
self.play(square.animate.move_to(np.array([1.0, 1.0, 0.0])))
class ShiftTest(Scene):
def construct(self):
square = Square()
self.play(square.shift, UP)
self.play(square.animate.shift(UP))
MODULE_NAME = "movements"

View file

@ -75,7 +75,7 @@ class RestoreTest(Scene):
circle = Circle()
self.play(Transform(square, circle))
square.save_state()
self.play(square.shift, UP)
self.play(square.animate.shift(UP))
self.play(Restore(square))

View file

@ -12,7 +12,7 @@ class UpdaterTest(Scene):
self.add(dot, square)
square.add_updater(lambda m: m.next_to(dot, RIGHT, buff=SMALL_BUFF))
self.add(square)
self.play(dot.shift, UP * 2)
self.play(dot.animate.shift(UP * 2))
square.clear_updaters()