Skip to content

Instantly share code, notes, and snippets.

@abul4fia
Created March 11, 2024 08:56
Show Gist options
  • Save abul4fia/4537d5f257152b7980de4a50bda2cf53 to your computer and use it in GitHub Desktop.
Save abul4fia/4537d5f257152b7980de4a50bda2cf53 to your computer and use it in GitHub Desktop.
How Animation works (more or less)

How animations work (more or less)

To be frank, I don't know all the details about making your own custom Animation class. I know only a general idea, but the details are fuzzy. I usually end up using a trial-and-error approach trying different methods and reading the source code of similar animations, until it works. But the main idea is:

  1. An animation receives a mobject (it can be a group) on which it will operate. In the __init__ method usually it only stores that object into self.mobject, so every animation has that object, and other animation attributes such as the run time, rate function, etc.
  2. When used in a .play(), manim will call its begin() method. Usually you don't need to override that. The default implementation creates another inner object called self.starting_mobject which stores the initial state of the mobject being animated, and it is simply a deep copy of self.mobject (although you can override how that starting mobject is created by overriding create_starting_mobject() method.
  3. At each frame, manim will call the interpolate() method of the animation, passing it alpha. This parameter is between 0 and 1 an represents the "intermediate state" of the self.mobject to render. 0 means the initial state, 1 means the final state. So for example, if you are animating a rotation of 90 degrees, and alpha is 0.5 you have to set self.mobject to your starting_mobject rotated 45 degrees. Note that alpha can go from 0 to 1 and then back to 0, depending on the rate_func, so usually it is easier to implement the result as some manipulation of self.starting_mobject instead of relying on the last state of self.mobject
  4. When the animation reaches its end, finish() is called. The default implementation calls interpolate(1)
  5. Finally clean_up_from_scene() is called. The default implementation removes the object from the scene if the animation has the attribute self.remover to True (examples of these are FadeOut(), Uncreate(),...)

But indeed (and here is where things begin to get fuzzy), you don't need to override interpolate() either, because the default implementation simply calls interpolate_mobject(), with the same parameter alpha. So you can override this second one instead. Why is it like this? No idea.

Even more confusing, you don't need to override interpolate_mobject() either, specially for composed mobjects (ones that have submobjects) because the default implementation of interpolate_mobject() extracts all submobjects from self.mobject and calls interpolate_submobject() passing it three parameters: submobject (the one to be updated), starting_submobject (the initial state of that one) and alpha.

Usually it is interpolate_submobject() the one that you have to override.

So what I usually do is to inherit from some animations that does something similar to what I want to do, and the override some of those methods.

A pretty generic animation from which inherit is Transform. This one does the following:

  1. At __init__ it receives two mobjects instead of one. It stores them in self.mobject and self.target_mobject
  2. At begin() it calls self.create_target() to initialize self.target_mobject (by default this function simply returns the self.target_mobject that was stored in __init__). Then it creates a copy of self.target_mobject into self.target_copy
  3. It does not override interpolate, but interpolate_submobject() instead. From this method it simply calls submobject.interpolate() passing it the starting submobject, the target copy and the alpha. It is responsability of submobject to "morph" itself into something between the starting and the target, governed by alpha.
  4. It does not override finish()
  5. In clean_up_from_scene() it does the replacement of the original mobject by the transformed one, if replace_mobject_with_target_in_scene was True (it is for ReplacementTransform()).

Example

I want an animation similar to ShrinkToCenter() but that shrinks into any other given point, instead of the center.

An easy way is to transform the object into that final point, so we can extend Transform and override the method that provides the target:

class ShrinkToPoint(Transform):
    """Remove an :class:`~.Mobject` by shrinking it to a point.

    Parameters
    ----------
    mobject
        The mobjects to be introduced.
    point
        The point to which the mobject shrinks.
    """

    def __init__(
        self, mobject: Mobject, point: np.ndarray, point_color: str = WHITE, **kwargs
    ) -> None:
        self.point = point
        self.point_color = point_color
        super().__init__(mobject, remover=True, **kwargs)

    def create_target(self) -> Mobject:
        super().create_target()
        return self.mobject.copy().move_to(self.point).scale(0)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment