Cobweb

slama.dev

Cobweb

Manim Icon Groups, Transforms, Updaters

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 zz 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)(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.

To create a circle given three of its points, the Circle.from_three_points may be used.

Author’s Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
from manim import *
from random import *


class Triangle(Scene):
    def construct(self):
        seed(0xDEADBEEF)

        # scale everything up a bit
        c = 2

        p1 = 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)

        def create_line_updater(a, b):
            """Returns a function that acts as an updater for the given segment."""
            return lambda x: 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(lambda x: x.next_to(p1, UP))
        y.add_updater(lambda x: x.next_to(p2, DOWN + LEFT))
        z.add_updater(lambda x: 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(
            lambda c: 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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#######################################################
#  #################            ##                    #
# ##################           ####                   #
# #################            ####                   #
#  ###############             #####               # ##
#      #########               #####               ####
#         ###                  ######            ######
#          ###            ##   #####    ###       #####
#          ####      ########   ####  #####        ## #
#          #####   ##########   ###  ########       # #
#         #####   ###########        ########         #
#         ####   ###########        ##########        #
#        ##      ###########        ##########        #
#      ####     ############      #############       #
#    ######     ############     #############        #
# #########  ## ###########     #########    #        #
# ############### #########     #######               #
# ###############   ######      ######                #
# ###############    #####       ####                 #
#   #############      #                ##            #
#     #  #######                       ########### ####
#          ###         #              #################
# ##                  ####            #################
#####                ######          ##################
######                ######         ##################
# ###      ###        #######  ###   ###############  #
#         ####         ############   ####  #######   #
#        #####          ############          ###     #
#         ###            ##########                   #
#######################################################
Author’s Solution
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
from manim import *
from random import *


class Wave(Scene):
    def construct(self):
        seed(0xDEADBEEF)

        maze_string = """
#######################################################
#  #################            ##                    #
# ##################           ####                   #
# #################            ####                   #
#  ###############             #####               # ##
#      #########               #####               ####
#         ###                  ######            ######
#          ###            ##   #####    ###       #####
#          ####      ########   ####  #####        ## #
#          #####   ##########   ###  ########       # #
#         #####   ###########        ########         #
#         ####   ###########        ##########        #
#        ##      ###########        ##########        #
#      ####     ############      #############       #
#    ######     ############     #############        #
# #########  ## ###########     #########    #        #
# ############### #########     #######               #
# ###############   ######      ######                #
# ###############    #####       ####                 #
#   #############      #                ##            #
#     #  #######                       ########### ####
#          ###         #              #################
# ##                  ####            #################
#####                ######          ##################
######                ######         ##################
# ###      ###        #######  ###   ###############  #
#         ####         ############   ####  #######   #
#        #####          ############          ###     #
#         ###            ##########                   #
#######################################################
"""

        maze = []  # 2D array of squares like we see it
        maze_bool = []  # 2D array of true/false values
        all_squares = VGroup()

        # go line by line
        for row in maze_string.strip().splitlines():
            maze.append([])
            maze_bool.append([])

            for char in row:
                square = Square(
                    side_length=0.23,
                    stroke_width=1,
                    fill_color=WHITE if char == "#" else BLACK,
                    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 grid
        all_squares.arrange_in_grid(rows=h, buff=0)

        self.play(FadeIn(all_squares), run_time=2)

        x, y = 1, 1

        colors = ["#ef476f", "#ffd166", "#06d6a0", "#118ab2"]

        # create a dictionary of distances from start to other points
        distances = {(x, y): 0}
        stack = [(x, y, 0)]

        while len(stack) != 0:
            x, y, d = stack.pop(0)

            for dx, dy in ((0, 1), (1, 0), (-1, 0), (0, -1)):
                nx, ny = dx + x, dy + y

                if nx < 0 or nx >= w or ny < 0 or ny >= h:
                    continue

                if maze_bool[ny][nx] and (nx, ny) not in distances:
                    stack.append((nx, ny, d + 1))
                    distances[(nx, ny)] = d + 1

        max_distance = max([d for d in distances.values()])

        all_colors = color_gradient(colors, max_distance + 1)

        # create animation groups for each distance from start
        groups = []
        for d in range(max_distance + 1):
            groups.append(
                AnimationGroup(
                    *[
                        maze[y][x].animate.set_fill(all_colors[d])
                        for x, y in distances
                        if distances[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.

Author’s Solution
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
from manim import *


class Path(VMobject):
    def __init__(self, points, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.set_points_as_corners(points)

    def get_important_points(self):
        return list(self.get_start_anchors()) + [self.get_end_anchors()[-1]]


class Hilbert(Scene):
    def construct(self):
        points = [LEFT + DOWN, LEFT + UP, RIGHT + UP, RIGHT + DOWN]

        hilbert = Path(points).scale(3)

        self.play(Create(hilbert), rate_func=linear)

        for i in range(1, 6):
            # length of a single segment in the curve
            new_segment_length = 1 / (2 ** (i + 1) - 1)

            # scale the curve such that it it is centered
            new_scale = (1 - new_segment_length) / 2

            # save the previous (large) curve to align smaller ones by it
            lu = hilbert.copy()
            lu, hilbert = hilbert, lu

            self.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 nice
            self.play(Create(new_hilbert, run_time=1.5 ** (i - 1)), rate_func=linear)

            self.remove(lu, ru, ld, rd)

            hilbert = new_hilbert