slama.dev

Manim – 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.

from manim import *


class VGroupExample(Scene):
    def construct(self):
        s1 = Square(color=RED)
        s2 = Square(color=GREEN)
        s3 = Square(color=BLUE)

        s1.next_to(s2, LEFT)
        s3.next_to(s2, RIGHT)

        self.play(Write(s1), Write(s2), Write(s3))

        group = VGroup(s1, s2, s3)

        # scale the entire group
        self.play(group.animate.scale(1.5).shift(UP))

        # only work with one of the group's objects
        self.play(group[1].animate.shift(DOWN * 2))

        # change color and fill
        self.play(group.animate.set_color(WHITE))
        self.play(group.animate.set_fill(WHITE, 1))

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

from manim import *
from random import seed, uniform


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

        circles = VGroup(
            *[
                Circle(radius=0.1)
                .scale(uniform(0.5, 4))
                .shift(UP * uniform(-3, 3) + RIGHT * uniform(-5, 5))
                for _ in range(12)
            ]
        )

        self.play(FadeIn(circles))

        # left-to-right arrangement
        self.play(circles.animate.arrange())

        # specify the direction of arrangement and spacing between the objects
        self.play(circles.animate.arrange(direction=DOWN, buff=0.3))
        self.play(circles.animate.arrange(direction=LEFT + DOWN, buff=0.1))

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.

from manim import *
from random import seed, uniform


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

        circles = VGroup(
            *[
                Circle(radius=0.1)
                .scale(uniform(0.5, 2))
                .shift(UP * uniform(-3, 3) + RIGHT * uniform(-5, 5))
                for _ in range(9 ** 2)
            ]
        )

        self.play(FadeIn(circles))

        # square grid (or as close as possible)
        self.play(circles.animate.arrange_in_grid())

        # different parameters for rows and columns
        self.play(circles.animate.arrange_in_grid(rows=5, buff=0))
        self.play(circles.animate.arrange_in_grid(cols=12, buff=0.3))

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.

from manim import *


class AddRemoveExample(Scene):
    def construct(self):
        square = Square(fill_color=WHITE, fill_opacity=1)
        small_scale = 0.6

        triangle = Triangle(fill_opacity=1).scale(small_scale).move_to(square)

        self.play(Write(square))

        # add a triangle behind the square
        self.bring_to_back(triangle)
        self.play(square.animate.shift(LEFT * 2))

        circle = Circle(fill_opacity=1).scale(small_scale).move_to(square)

        # add a circle behind the square
        self.bring_to_back(circle)
        self.play(square.animate.shift(RIGHT * 2))

        square2 = (
            Square(stroke_color=GREEN, fill_color=GREEN, fill_opacity=1)
            .scale(small_scale)
            .move_to(square)
        )

        self.remove(triangle)

        # add a second square in front of the square
        # looks jarring but is just to show the functionality
        self.bring_to_front(square2)

        self.play(square.animate.shift(RIGHT * 2))

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

from manim import *


class ZIndexExample(Scene):
    def construct(self):
        c1 = Circle(fill_opacity=1, fill_color=RED, stroke_width=5, stroke_color=WHITE).shift(LEFT)
        c2 = Circle(fill_opacity=1, fill_color=GREEN, stroke_width=5, stroke_color=WHITE)
        c3 = Circle(fill_opacity=1, fill_color=BLUE, stroke_width=5, stroke_color=WHITE).shift(RIGHT)

        self.add(c1, c2, c3)

        self.wait(1)

        # we'll increase the z index of c1 and c2, which will bring to the front
        # c2 will still be in front of c1, which is the order they are in the scene
        c1.set_z_index(1)
        c2.set_z_index(1)

        self.wait(1)

        # c1 will now be in front of both c2 (z = 1) and c3 (z = 0)
        c1.set_z_index(2)

        self.wait(1)

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.

from manim import *


class AnimationGroupExample(Scene):
    def construct(self):
        c1 = Square(color=RED)
        c2 = Square(color=GREEN)
        c3 = Square(color=BLUE)

        VGroup(c1, c2, c3).arrange(buff=1)

        # each animation starts in the quarter of the previous one
        self.play(AnimationGroup(Write(c1), Write(c2), Write(c3), lag_ratio=0.25))

        # each animation starts after the first tenth of the previous one
        self.play(AnimationGroup(FadeOut(c1), FadeOut(c2), FadeOut(c3), lag_ratio=0.1))

        # one animation can also be a group, which has different lag_ratio
        self.play(
            AnimationGroup(
                AnimationGroup(Write(c1), Write(c2), lag_ratio=0.1),
                Write(c3),
                lag_ratio=0.5,
            )
        )

        # lag_ratio can also be negative (in which case the animations run in reverse)
        # however, just because it can doesn't mean it should!
        self.play(AnimationGroup(FadeOut(c1), FadeOut(c2), FadeOut(c3), lag_ratio=-0.1))

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.

from manim import *


class VGroupLagRatioExample(Scene):
    def construct(self):
        squares = VGroup(Square(), Square(), Square()).arrange(buff=0.5).scale(1.5)

        # Write has non-zero lag_ratio by default so squares are written with overlap
        self.play(Write(squares))

        # FadeOut has zero lag_ratio by default, so all squares fade concurrently
        self.play(FadeOut(squares))

        squares.set_color(BLUE)

        self.play(Write(squares, lag_ratio=0))

        self.play(FadeOut(squares, lag_ratio=0.5))

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.

from manim import *


class AttentionExample(Scene):
    def construct(self):
        c1 = Square()

        labels = [
            Tex("Flash"),
            Tex("Indicate"),
            Tex("Wiggle"),
            Tex("FocusOn"),
            Tex("Circumscribe"),
        ]

        # move labels below the square
        for label in labels:
            label.next_to(c1, DOWN).scale(1.5)

        def switch_labels(i: int):
            """Animate switching one label for another. Notice the shift parameter!"""
            return AnimationGroup(
                FadeOut(labels[i], shift=UP * 0.5),
                FadeIn(labels[i + 1], shift=UP * 0.5),
            )

        self.play(Write(c1))

        self.play(FadeIn(labels[0], shift=UP * 0.5), c1.animate.shift(UP))

        self.play(Flash(c1, flash_radius=1.6, num_lines=20))

        self.play(AnimationGroup(switch_labels(0), Indicate(c1), lag_ratio=0.7))

        self.play(AnimationGroup(switch_labels(1), Wiggle(c1), lag_ratio=0.7))

        self.play(AnimationGroup(switch_labels(2), FocusOn(c1), lag_ratio=0.7))

        self.play(AnimationGroup(switch_labels(3), Circumscribe(c1), lag_ratio=0.7))

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.

from manim import *


class BasicTransformExample(Scene):
    def construct(self):
        c = Circle().scale(2)
        s = Square().scale(2)

        self.play(Write(c))

        self.play(Transform(c, s))

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.

from manim import *


class BadTransformExample(Scene):
    def construct(self):
        good = [Circle(color=GREEN), Square(color=GREEN), Triangle(color=GREEN)]
        bad = [Circle(color=RED), Square(color=RED), Triangle(color=RED)]

        VGroup(*(good + bad)).arrange_in_grid(rows=2, buff=1)

        self.play(Write(good[0]), Write(bad[0]))

        self.play(
            Transform(good[0], good[1]),  # o1 -> o2
            Transform(bad[0], bad[1]),    # o1 -> o2
        )

        self.play(
            Transform(good[0], good[2]),  # o1 -> o3
            Transform(bad[1], bad[2]),    # o2 -> o3 - bad!
        )

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!

from manim import *


class TransformMatchingShapesExample(Scene):
    def construct(self):
        rr = Tex("WYAY").scale(5)

        # \parbox is a TeX command for setting paragraphs of certain width
        rr_full = Tex(
            # kindly ignore the contents of this string
            r"""\parbox{20em}{We're no strangers to love.
            You know the rules and so do I.
            A full commitment's what I'm thinking of.
            You wouldn't get this from any other guy.}"""
        )

        self.play(Write(rr))

        self.play(TransformMatchingShapes(rr, rr_full))

        # careful! behaves the same as ReplacementTransform, so we need to use rr_full
        self.play(FadeOut(rr_full))

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.

from manim import *


class SimpleUpdaterExample(Scene):
    def construct(self):
        square = Square()
        square_label = Tex("A neat square.").next_to(square, UP, buff=0.5)

        self.play(Write(square))
        self.play(FadeIn(square_label, shift=UP * 0.5))

        def label_updater(obj):
            """An updater which continually move an object above the square.

            The first parameter (obj) is always the object that is being updated."""
            obj.next_to(square, UP, buff=0.5)

        # we want the label to always reside above the square
        square_label.add_updater(label_updater)

        self.play(square.animate.shift(LEFT * 3))
        self.play(square.animate.scale(1 / 2))
        self.play(square.animate.rotate(PI / 2).shift(RIGHT * 3 + DOWN * 0.5).scale(3))

        # to pause updaters, we'll use suspend_updating()
        square_label.suspend_updating()

        self.play(square.animate.scale(1 / 3))
        self.play(square.animate.rotate(PI / 2))

        # to resume,we'll use resume__updating()
        square_label.resume_updating()

        self.play(square.animate.scale(3))
        self.play(square.animate.rotate(PI / 2))

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

from manim import *


class BecomeUpdaterExample(Scene):
    def format_point(self, point) -> str:
        """Format the given point to look presentable."""
        return f"[{point[0]:.2f}, {point[1]:.2f}]"

    def construct(self):
        circle = Circle(color=WHITE)

        def circle_label_updater(obj):
            """An updater that displays the circle's position above it."""
            obj.become(Tex(f"p = {self.format_point(circle.get_center())}"))
            obj.next_to(circle, UP, buff=0.35)

        self.play(Write(circle))

        circle_label = Tex()

        # a bit of a hack - we're calling the updater to create the initial label
        circle_label_updater(circle_label)

        self.play(FadeIn(circle_label, shift=UP * 0.3))

        # start updating the label
        circle_label.add_updater(circle_label_updater)

        self.play(circle.animate.shift(RIGHT))
        self.play(circle.animate.shift(LEFT * 3 + UP))
        self.play(circle.animate.shift(DOWN * 2 + RIGHT * 2))
        self.play(circle.animate.shift(UP))

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.

from manim import *


class LineExample(Scene):
    def construct(self):
        p1 = Dot()
        p2 = Dot()

        points = VGroup(p1, p2).arrange(buff=2.5)

        line = Line(start=p1.get_center(), end=p2.get_center())

        self.play(Write(p1), Write(p2))

        self.play(Write(line))

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

from manim import *


class CircleFromPointsExample(Scene):
    def construct(self):
        p1 = Dot().shift(LEFT + UP)
        p2 = Dot().shift(DOWN * 1.5)
        p3 = Dot().shift(RIGHT + UP)

        dots = VGroup(p1, p2, p3)

        # create a circle from three points
        circle = Circle.from_three_points(p1.get_center(), p2.get_center(), p3.get_center(), color=WHITE)

        self.play(Write(dots), run_time=1.5)
        self.play(Write(circle))
Author's Solution
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.

from manim import *


class ColorGradientExample(Scene):
    def construct(self):
        rows = 6
        square_count = rows * 9

        # the colors can be either the built-in constants or in hex notation
        # (the builtin ones are just strings in the hex notation too!)
        colors = [RED, "#ffd166", "#06d6a0", BLUE]
        squares = [
            Square(fill_color=WHITE, fill_opacity=1).scale(0.3)
            for _ in range(square_count)
        ]

        group = VGroup(*squares).arrange_in_grid(rows=rows)

        self.play(Write(group, lag_ratio=0.04))

        all_colors = color_gradient(colors, square_count)

        self.play(
            AnimationGroup(
                *[s.animate.set_color(all_colors[i]) for i, s in enumerate(squares)],
                lag_ratio=0.02,
            )
        )

        self.play(FadeOut(group))

Here is the text input that I used to generate the maze, if you wish to use it.

#######################################################
#  #################            ##                    #
# ##################           ####                   #
# #################            ####                   #
#  ###############             #####               # ##
#      #########               #####               ####
#         ###                  ######            ######
#          ###            ##   #####    ###       #####
#          ####      ########   ####  #####        ## #
#          #####   ##########   ###  ########       # #
#         #####   ###########        ########         #
#         ####   ###########        ##########        #
#        ##      ###########        ##########        #
#      ####     ############      #############       #
#    ######     ############     #############        #
# #########  ## ###########     #########    #        #
# ############### #########     #######               #
# ###############   ######      ######                #
# ###############    #####       ####                 #
#   #############      #                ##            #
#     #  #######                       ########### ####
#          ###         #              #################
# ##                  ####            #################
#####                ######          ##################
######                ######         ##################
# ###      ###        #######  ###   ###############  #
#         ####         ############   ####  #######   #
#        #####          ############          ###     #
#         ###            ##########                   #
#######################################################
Author's Solution
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.

from manim import *


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

    def get_important_points(self):
        """Returns the important points of the curve."""
        # shot explanation: Manim uses quadratic Bézier curves to create paths
        # > each curve is determined by 4 points - 2 anchor and 2 control
        # > VMobject's builtin self.points returns *all* points
        # > we, however, only care about the anchors
        # > see https://en.wikipedia.org/wiki/Bézier_curve for more details
        return list(self.get_start_anchors()) + [self.get_end_anchors()[-1]]


class PathExample(Scene):
    def construct(self):
        path = Path([LEFT + UP, LEFT + DOWN, RIGHT + UP, RIGHT + DOWN], color=WHITE)

        self.play(Write(path))

        path_points = VGroup(*[Dot().move_to(point) for point in path.get_important_points()])

        self.play(Write(path_points))

        path2 = path.copy()
        path3 = path.copy()

        self.play(
            path2.animate.next_to(path, LEFT, buff=1),
            path3.animate.next_to(path, RIGHT, buff=1),
        )

        # flip(LEFT) flips top-down, because LEFT is the axis **by which** to flip
        self.play(
            path2.animate.flip(),
            path3.animate.flip(LEFT),
        )

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.

from manim import *


class WriteVsCreate(Scene):
    def construct(self):
        s1 = Square(stroke_width=5)
        t1 = Tex("Write")

        s2 = Square(stroke_width=5)
        t2 = Tex("Create")

        VGroup(
            VGroup(s1, t1).arrange(DOWN),
            VGroup(s2, t2).arrange(DOWN),
        ).arrange(buff=1)

        # write also animates the outline
        self.play(FadeIn(t1))
        self.play(Write(s1, run_time=2))
        self.play(s1.animate.set_color(DARK_GRAY))

        # create does not and is therefore better suited
        # the rate_func parameter is magic for now and is covered in the next part
        self.play(FadeIn(t2))
        self.play(Create(s2, run_time=2, rate_func=linear))
        self.play(s2.animate.set_color(DARK_GRAY))

        self.wait()
Author's Solution
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