In this part of the series, we’ll learn a number of useful functions and classes when working with groups of objects.
We’ll also learn how to transform objects into others, how updaters work and a few geometry-related things.
Grouping objects
It is sometimes more useful to work with a group of objects as one, for example when moving them, scaling them, changing their color, etc.
This is where the VGroup
class comes in.
The VGroup
class additionally contains functions for arranging objects.
The simplest to use is the arrange
function which arranges object such that they are next to one another, in the order they were passed to the constructor (left to right).
The buff parameter determines the spacing between the objects and can be used in most of the functions that involve positioning objects next to one another, such as the next_to
function that we’ve seen already.
The direction parameter determines the direction in which the objects are arranged.
A more general version of the arrange
function is the arrange_in_grid
function, which (as the name suggests) arranges the objects of the group in a grid. By default, it attempts to make the grid as square as possible.
Adding and removing elements
To add objects to the scene instantly (without animating), we can use the bring_to_front
(or alternatively add
, which is the same) and bring_to_back
functions.
To remove them, we can use the remove
function.
We’ll cover the scene functionality in-depth in one of the upcoming parts in the series.
For now, it’s sufficient to know that the scene objects have a set order in which they are rendered, which is determined primarily by their z_index (something different than their z coordinate) and secondarily by the order they were added to the scene (newer objects on top, older on the bottom).
Overlapping animations
We’ve seen how to start animations concurrently, but it is often prettier to run them with a certain overlap (especially when there is a lot of them), which makes them look smoother.
To achieve this, we’ll use the AnimationGroup
object with a lag_ratio parameter, which determines the part of a given animation when the next one is started.
Animation overlap also works for objects in a VGroup
when applying animations that have non-zero lag_ratio (such as Write
), in which case it is applied in the order they were added to the group.
Working with attention
When creating animations, it is sometimes necessary to draw attention to a certain part of the screen where the viewer’s focus should be.
Manim offers a number of ways how to do this, the most common of which are Flash
, Indicate
, Wiggle
, FocusOn
and Circumscribe
.
The Flash
, Indicate
and Circumscribe
animations have a useful optional color parameter, which changes the default color from yellow to whatever you prefer.
Also, notice that we used an optional shift parameter for FadeIn
and FadeOut
which specify the direction in which the fade should occur.
Transformations
Manim can animate the transformation of one object to another in a number of different ways.
The most commonly used is Transform
, which smoothly transforms one object onto another.
It is, however, important to pay attention to which object we’re working with when doing multiple transformations – we still have to work with the original object or things will go wrong.
If this behavior feels unintuitive, it is possible to use ReplacementTransform
, which swaps the behavior above (good will be bad and vice versa).
Besides these transformations, it is also possible to use TransformMatchingShapes
, which attempts to transform the objects in a manner that preserves parts that they have in common.
Note that it has the same behavior as ReplacementTransform
!
Updaters
Imagine that we would like to continually update an object during an animation (its position, scale, etc.).
This what Manim’s updaters are for – they are functions containing arbitrary code, connected to a certain object and evaluated each rendered frame.
Besides changing certain attributes of an object, it might be useful to transform it to an entirely new one.
We’ll use the become
function, which transforms an object into another one immediately (not like Transform
, which is an animation).
Besides the new become
function, the code also contains the get_center
function, which returns a NumPy array with the (x,y,z) coordinates of the object (in our case the circle).
Tasks
Triangle
Create an animation of the following triangle.
Dots and segments can be defined using the Dot
and Line
classes.
frommanimimport*fromrandomimport*classTriangle(Scene):defconstruct(self):seed(0xDEADBEEF)# scale everything up a bitc=2p1=Dot().scale(c).shift(UP*c)p2=Dot().scale(c).shift(DOWN*c+LEFT*c)p3=Dot().scale(c).shift(DOWN*c+RIGHT*c)points=VGroup(p1,p2,p3)self.play(Write(points,lag_ratio=0.5),run_time=1.5)l1=Line()l2=Line()l3=Line()lines=VGroup(l1,l2,l3)defcreate_line_updater(a,b):"""Returns a function that acts as an updater for the given segment."""returnlambdax:x.become(Line(start=a.get_center(),end=b.get_center()))l1.add_updater(create_line_updater(p1,p2))l2.add_updater(create_line_updater(p2,p3))l3.add_updater(create_line_updater(p3,p1))self.play(Write(lines,lag_ratio=0.5),run_time=1.5)x=Tex("x")y=Tex("y")z=Tex("z")x.add_updater(lambdax:x.next_to(p1,UP))y.add_updater(lambdax:x.next_to(p2,DOWN+LEFT))z.add_updater(lambdax:x.next_to(p3,DOWN+RIGHT))labels=VGroup(x,y,z).scale(c*0.8)self.play(FadeIn(labels,shift=UP*0.2))circle=Circle()circle.add_updater(lambdac:c.become(Circle.from_three_points(p1.get_center(),p2.get_center(),p3.get_center())))self.play(Write(circle))self.play(p2.animate.shift(LEFT+UP),p1.animate.shift(RIGHT),)
Wave
Create an animation of a smooth wave running through a maze.
The arrange_in_grid
will be useful to position the squares.
To overlap the coloring of each layer in the maze, nested AnimationGroup
can be used.
For creating a smooth gradient from the input colors, the color_gradient
utility function is quite useful.
Here is the text input that I used to generate the maze, if you wish to use it.
frommanimimport*fromrandomimport*classWave(Scene):defconstruct(self):seed(0xDEADBEEF)maze_string="""
#######################################################
# ################# ## #
# ################## #### #
# ################# #### #
# ############### ##### # ##
# ######### ##### ####
# ### ###### ######
# ### ## ##### ### #####
# #### ######## #### ##### ## #
# ##### ########## ### ######## # #
# ##### ########### ######## #
# #### ########### ########## #
# ## ########### ########## #
# #### ############ ############# #
# ###### ############ ############# #
# ######### ## ########### ######### # #
# ############### ######### ####### #
# ############### ###### ###### #
# ############### ##### #### #
# ############# # ## #
# # ####### ########### ####
# ### # #################
# ## #### #################
##### ###### ##################
###### ###### ##################
# ### ### ####### ### ############### #
# #### ############ #### ####### #
# ##### ############ ### #
# ### ########## #
#######################################################
"""maze=[]# 2D array of squares like we see itmaze_bool=[]# 2D array of true/false valuesall_squares=VGroup()# go line by lineforrowinmaze_string.strip().splitlines():maze.append([])maze_bool.append([])forcharinrow:square=Square(side_length=0.23,stroke_width=1,fill_color=WHITEifchar=="#"elseBLACK,fill_opacity=1,)maze[-1].append(square)maze_bool[-1].append(char==" ")all_squares.add(square)w=len(maze[0])h=len(maze)# arrange the squares in the gridall_squares.arrange_in_grid(rows=h,buff=0)self.play(FadeIn(all_squares),run_time=2)x,y=1,1colors=["#ef476f","#ffd166","#06d6a0","#118ab2"]# create a dictionary of distances from start to other pointsdistances={(x,y):0}stack=[(x,y,0)]whilelen(stack)!=0:x,y,d=stack.pop(0)fordx,dyin((0,1),(1,0),(-1,0),(0,-1)):nx,ny=dx+x,dy+yifnx<0ornx>=worny<0orny>=h:continueifmaze_bool[ny][nx]and(nx,ny)notindistances:stack.append((nx,ny,d+1))distances[(nx,ny)]=d+1max_distance=max([dfordindistances.values()])all_colors=color_gradient(colors,max_distance+1)# create animation groups for each distance from startgroups=[]fordinrange(max_distance+1):groups.append(AnimationGroup(*[maze[y][x].animate.set_fill(all_colors[d])forx,yindistancesifdistances[x,y]==d]))self.play(AnimationGroup(*groups,lag_ratio=0.08))
Hilbert
Create an animation of the Hilbert’s (or any other space-filling) curve.
You can use this custom Path function, which creates a path from the given points.
We’re also using the flip
function, which flips an object in the given axis (defaulting to flipping left-right), and the copy
function, which copies an object.
Furthermore, the Create
animation is more appropriate than Write
since we’re not interested in the outline of the curve we’re drawing, only its shape.
frommanimimport*classPath(VMobject):def__init__(self,points,*args,**kwargs):super().__init__(*args,**kwargs)self.set_points_as_corners(points)defget_important_points(self):returnlist(self.get_start_anchors())+[self.get_end_anchors()[-1]]classHilbert(Scene):defconstruct(self):points=[LEFT+DOWN,LEFT+UP,RIGHT+UP,RIGHT+DOWN]hilbert=Path(points).scale(3)self.play(Create(hilbert),rate_func=linear)foriinrange(1,6):# length of a single segment in the curvenew_segment_length=1/(2**(i+1)-1)# scale the curve such that it it is centerednew_scale=(1-new_segment_length)/2# save the previous (large) curve to align smaller ones by itlu=hilbert.copy()lu,hilbert=hilbert,luself.play(lu.animate.scale(new_scale).set_color(DARK_GRAY).align_to(hilbert,points[1]))ru=lu.copy()self.play(ru.animate.align_to(hilbert,points[2]))ld,rd=lu.copy(),ru.copy()self.play(ld.animate.align_to(hilbert,points[0]).rotate(-PI/2),rd.animate.align_to(hilbert,points[3]).rotate(PI/2),)new_hilbert=Path(list(ld.flip(LEFT).get_important_points())+list(lu.get_important_points())+list(ru.get_important_points())+list(rd.flip(LEFT).get_important_points()))# Create will be exponentially longer so it looks niceself.play(Create(new_hilbert,run_time=1.5**(i-1)),rate_func=linear)self.remove(lu,ru,ld,rd)hilbert=new_hilbert