This part of the series covers creating custom objects and animations.
This part used to also include plugins, but all of the original ones are broken as of v0.18.0.
Custom objects
For more complex scenes, it might be a good idea to create custom Manim objects.
These usually reperesent more complex objects where simple primitives don’t suffice.
To see how to create them, let’s look at an example of a stack:
frommanimimport*classStack(VMobject):def__init__(self,size,**kwargs):# initialize the vmobjectsuper().__init__(**kwargs)self.squares=VGroup()self.labels=VGroup()self.index=0self.pointer=Arrow(ORIGIN,UP*1.2)for_inrange(size):self.squares.add(Square(side_length=0.8))self.squares.arrange(buff=0.15)self.pointer.next_to(self.squares[0],DOWN)# IMPORTANT - we have to add all of the subobjects for them to be displayedself.add(self.squares,self.pointer,self.labels)def__peek(self)->VMobject:"""Return the current top element in the stack."""returnself.squares[self.index]def__create_label(self,element)->VMobject:"""Create the label for the given element (given its color and size)."""return(Tex(str(element)).set_height(self.__peek().height/2).set_color(self.__peek().get_color()).move_to(self.__peek()).set_z_index(1)# labels on top!)def__animate_indicate(self,element,increase:bool=True)->Animation:"""Return an animation indicating the current element."""returnIndicate(element,color=self.__peek().get_color(),scale_factor=1.1ifincreaseelse1/1.1,)defpush(self,element)->Animation:"""Pushes an element onto the stack, returning an appropriate animation."""label=self.__create_label(element)self.labels.add(label)self.index+=1returnAnimationGroup(FadeIn(label),self.pointer.animate.next_to(self.__peek(),DOWN),self.__animate_indicate(self.squares[self.index-1],increase=True),)defpop(self)->AnimationGroup:"""Pops an element from the stack, returning an appropriate animation."""label=self.labels[-1]self.labels.remove(label)self.index-=1returnAnimationGroup(FadeOut(label),self.pointer.animate.next_to(self.__peek(),DOWN),self.__animate_indicate(self.__peek(),increase=False),)defclear(self)->AnimationGroup:"""Clear the entire stack, returning the appropriate animation."""result=Succession(*[self.pop()for_inrange(self.index)])self.index=0returnresultclassStackExample(Scene):defconstruct(self):stack=Stack(10)self.play(Write(stack))self.wait(0.5)foriinrange(5):self.play(stack.push(i))self.play(stack.pop())self.wait(0.5)# we can even use the animate syntax!self.play(stack.animate.scale(1.2).set_color(BLUE))self.wait(0.5)foriinrange(2):self.play(stack.push(i))self.play(stack.pop())self.play(stack.clear())self.play(FadeOut(stack))
As the code suggests, every custom Manim object must inherit the Mobject
class (or VMobject
, if it’s a vector object).
The stack consists of other Manim objects, added to it via the add
method.
And, since it’s a regular Manim object, we can interact with it like we would with any other Manim object.
Custom animations
Custom animations are again very useful when you are dealing with more complex scenes or encounter the limitations of the builtin ones (like our MoveAndFade animation from the previous part).
To see how to create them, it is again best to look at an example:
frommanimimport*classColorfulFadeIn(Animation):"""An animation that fades in an object... but colorfully."""def__init__(self,mobject:Mobject,introducer=True,**kwargs):# we're using the introducer keyword argument# because the animation adds the objects to the scene# the original version of the object will be useful,# since we'll be changing itself.original=mobject.copy()super().__init__(mobject,introducer=introducer,**kwargs)definterpolate_mobject(self,alpha:float)->None:"""A function that gets called every frame, for the animation to... animate."""# the animation could have a custom rate function, but alpha is linear (0 to 1)# this means that we will have to apply it to get the appropriate behaviornew_alpha=self.rate_func(alpha)colors=["#ffd166",RED,"#06d6a0",BLUE]+[self.original.get_color()]fori,colorinenumerate(colors):ifi+1>=new_alpha*len(colors):new_color=interpolate_color(colors[i-1],colors[i],1-(i+1-new_alpha*len(colors)),)breaknew_mobject=self.original.copy().set_opacity(new_alpha).set_color(new_color)self.mobject.become(new_mobject)classRoll(Animation):"""A rolling animation."""def__init__(self,mobject:Mobject,angle,direction,scale_ratio=0.85,**kwargs):# the original version of the object will be useful, since we'll be changing itself.original=mobject.copy()self.scale_ratio=scale_ratioself.angle=angleself.direction=directionsuper().__init__(mobject,**kwargs)definterpolate_mobject(self,alpha:float)->None:"""A function that gets called every frame, for the animation to... animate."""actual_alpha=self.rate_func(alpha)# each function will have scale 1 in the beginning, scale_ration in the middle# and 1 at the end (probably not the most elegant way to achieve this)scale_alpha=1-(1-self.scale_ratio)*2*(0.5-abs(actual_alpha-0.5))# we want the object to move there and backdirection_alpha=there_and_back(actual_alpha)self.mobject.become(self.original.copy()).rotate(actual_alpha*self.angle).scale(scale_alpha).shift(self.direction*direction_alpha)classDissolve(AnimationGroup):"""An animation that dissolves an object (shrinks + flashes)."""def__init__(self,mobject:Mobject,remover=True,**kwargs):# we're using the remover keyword argument, because the animation adds the# objects to the scene (can be seen when running self.mobjects after)self.original=mobject.copy()a1=mobject.animate.scale(0)a2=Flash(mobject,color=mobject.color)super().__init__(a1,a2,lag_ratio=0.75,remover=remover,**kwargs)classStarFox(Scene):defconstruct(self):square=Square(color=BLUE,fill_opacity=0.75).scale(1.5)self.play(ColorfulFadeIn(square),run_time=3)self.play(Roll(square,angle=PI,direction=LEFT*0.75))self.play(Roll(square,angle=-PI,direction=RIGHT*0.75))self.play(Dissolve(square))
The example implements three new animations: ColorfulFadeIn, Roll and Dissolve.
The first (ColorfulFadeIn) inherits from the Animation
class and implements an interpolate_mobject
function, which is called every frame for the animation to play out. It is also an introducer (as seen from the introducer keyword argument), meaning that it “introduces” objects to the scene (important for animations to function properly).
The second (Roll) also inherits from the Animation
class.
The last (Dissolve) inherits from the AnimationGroup
(which itself inherits from the Animation
class) and is used to define animations made out of subanimations. It is also a remover (as again seen by the remover keyword argument), meaning that it “removes” objects from the scene (again, quite important for everything to work).