Команда Python for Devs подготовила перевод статьи о Manim — Python-инструменте для создания наглядных математических анимаций в стиле 3Blue1Brown. Разбираемся, как с помощью кода визуализировать уравнения, графики и абстрактные идеи так, чтобы они были понятны коллегам, менеджерам и студентам.


Мотивация

Случалось ли вам разбираться в математических концепциях, лежащих в основе алгоритмов машинного обучения, и в итоге обращаться к 3Blue1Brown как к учебному ресурсу? 3Blue1Brown — это популярный YouTube-канал о математике, созданный Грантом Сандерсоном, известный своими выдающимися объяснениями и впечатляющими анимациями.

А что если вы могли бы создавать похожие анимации, чтобы объяснять концепции data science своим коллегам по команде, менеджерам или подписчикам?

Грант разработал Python-пакет под названием Manim, который позволяет создавать математические анимации и изображения с помощью Python. В этой статье вы узнаете, как создавать красивые математические анимации с использованием Manim.

Полный исходный код и Jupyter Notebook для этого туториала доступны на GitHub. Склонируйте репозиторий, чтобы разбираться по ходу!

Что такое Manim?

Manim — это анимационный движок для создания точных и наглядных математических видео. Обратите внимание, что существует две версии Manim:

  1. Оригинальная версия, созданная Грантом Сандерсоном

  2. Версия, поддерживаемая сообществом

Поскольку версия Manim Community обновляется чаще и лучше протестирована, в этом туториале мы будем использовать именно её.

Чтобы установить зависимости пакета, перейдите к документации. После установки зависимостей выполните:

pip install manim

Создаём синий квадрат, растущий из центра

Начнём с создания синего квадрата, который растёт из центра:

from manim import *

class GrowingSquare(Scene):
    def construct(self):
        square = Square(color=BLUE, fill_opacity=0.5)
        self.play(GrowFromCenter(square))
        self.wait()

В этом коде используются четыре ключевых элемента Manim для создания анимации.

  • Scene: базовый класс, предоставляющий инфраструктуру для анимаций через construct() и play()

  • Square(color, fill_opacity): Mobject для создания квадратов с заданным цветом обводки и прозрачностью заливки

  • GrowFromCenter: анимация роста, начинающаяся из центра объекта
    wait(): пауза в таймлайне анимации длительностью 1 секунду

Сохраните этот скрипт в файл start.py. Теперь выполните команду ниже, чтобы сгенерировать видео для этого скрипта:

manim -p -ql start.py GrowingSquare

В вашей локальной директории будет сохранено видео с именем GrowingSquare.mp4. Вы увидите синий квадрат, который растёт из центра.

Пояснение параметров:

  • p: воспроизвести видео после завершения генерации

  • ql: сгенерировать видео в низком качестве

Чтобы создать видео в высоком качестве, используйте -qh.

Чтобы создать GIF вместо видео, добавьте параметр --format=gif к команде:

manim -p -ql --format=gif start.py GrowingSquare

Превращаем квадрат в круг

Создать просто квадрат — не слишком интересно. Давайте превратим квадрат в круг с помощью анимации Transform.

from manim import *

class SquareToCircle(Scene):
    def construct(self):
        circle = Circle()
        circle.set_fill(PINK, opacity=0.5)

        square = Square()
        square.rotate(PI / 4)

        self.play(Create(square))
        self.play(Transform(square, circle))
        self.play(FadeOut(square))

Этот код демонстрирует создание фигур, их стилизацию и применение различных анимационных эффектов.

  • Circle: создаёт круг

  • set_fill(color, opacity): задаёт цвет заливки и прозрачность (0.5 = 50% прозрачности)

  • rotate(angle): поворачивает объект на заданный угол (PI / 4 = 45 градусов)

  • Create: анимация, которая «рисует» фигуру на экране

  • Transform: плавно преобразует одну фигуру в другую

  • FadeOut: постепенно убирает объект с экрана

Полный список доступных фигур можно найти в документации Manim по геометрии.

Настройка Manim

Если вам не нужен чёрный фон, вы можете заменить его на белый с помощью свойства self.camera.background_color:

from manim import *

class CustomBackground(Scene):
    def construct(self):
        self.camera.background_color = WHITE

        square = Square(color=BLUE, fill_opacity=0.5)
        circle = Circle(color=RED, fill_opacity=0.5)

        self.play(Create(square))
        self.wait()
        self.play(Transform(square, circle))
        self.wait()

Другие способы настройки Manim можно найти в документации по конфигурации.

Запись математических уравнений с движущейся рамкой

Вы можете создавать анимации, в которых математические уравнения «выписываются» на экран, используя класс MathTex:

from manim import *

class WriteEquation(Scene):
    def construct(self):
        equation = MathTex(r"e^{i\pi} + 1 = 0")

        self.play(Write(equation))
        self.wait()

Этот код показывает, как записывать математические уравнения с помощью LaTeX.

  • MathTex: создаёт математические уравнения в нотации LaTeX (префикс r указывает, что используется raw-строка для LaTeX-команд)

  • Write: анимация, при которой текст появляется так, будто его пишут от руки

Также можно показывать пошаговое решение уравнений:

from manim import *

class EquationSteps(Scene):
    def construct(self):
        step1 = MathTex(r"2x + 5 = 13")
        step2 = MathTex(r"2x = 8")
        step3 = MathTex(r"x = 4")

        self.play(Write(step1))
        self.wait()
        self.play(Transform(step1, step2))
        self.wait()
        self.play(Transform(step1, step3))
        self.wait()

Разберём этот код:

  • Три этапа уравнения: отдельные объекты MathTex для каждого шага решения

  • Пошаговое преобразование: Transform последовательно преобразует первое уравнение в промежуточные и итоговое

  • Паузы по времени: wait() создаёт естественные паузы между шагами, подходящие для обучающего темпа

Вы также можете подсвечивать отдельные части уравнений с помощью рамок:

from manim import *

class MovingFrame(Scene):
    def construct(self):
        # Write equations
        equation = MathTex("2x^2-5x+2", "=", "(x-2)(2x-1)")

        # Create animation
        self.play(Write(equation))

        # Add moving frames
        framebox1 = SurroundingRectangle(equation[0], buff=.1)
        framebox2 = SurroundingRectangle(equation[2], buff=.1)

        # Create animations
        self.play(Create(framebox1))

        self.wait()
        # Replace frame 1 with frame 2
        self.play(ReplacementTransform(framebox1, framebox2))

        self.wait()

В этом коде используются следующие элементы:

  • MathTex с несколькими аргументами: разбивает уравнение на отдельные части, к которым можно обращаться и подсвечивать их по отдельности

  • SurroundingRectangle(mobject, buff): создаёт рамку вокруг части уравнения (buff=0.1 задаёт расстояние между рамкой и содержимым)

  • ReplacementTransform: плавно перемещает и преобразует одну рамку в другую, заменяя первую второй

Движение и масштабирование камеры

Вы можете управлять камерой и выбирать, на какую часть уравнения приблизиться, используя класс, унаследованный от MovingCameraScene:

from manim import *

class MovingCamera(MovingCameraScene):
    def construct(self):
        equation = MathTex(
            r"\frac{d}{dx}(x^2) = 2x"
        )

        self.play(Write(equation))
        self.wait()

        # Zoom in on the derivative
        self.play(
            self.camera.frame.animate.scale(0.5).move_to(equation[0])
        )
        self.wait()

Этот код показывает, как приближать отдельные элементы анимации.

  • MovingCameraScene: специальный тип сцены, который позволяет перемещать и масштабировать камеру

  • self.camera.frame: представляет область обзора камеры, которую можно перемещать и масштабировать

  • animate.scale(0.5): приближает изображение, уменьшая область обзора камеры на 50% (объекты выглядят в два раза больше)

  • move_to(equation[0]): центрирует камеру на первой части уравнения

Графики

С помощью Manim можно создавать аннотированные графики:

from manim import *

class Graph(Scene):
    def construct(self):
        axes = Axes(
            x_range=[-3, 3, 1],
            y_range=[-5, 5, 1],
            x_length=6,
            y_length=6,
        )

        # Add labels
        axes_labels = axes.get_axis_labels(x_label="x", y_label="f(x)")

        # Create a function graph
        graph = axes.plot(lambda x: x**2, color=BLUE)
        graph_label = axes.get_graph_label(graph, label="x^2")

        self.add(axes, axes_labels)
        self.play(Create(graph))
        self.play(Write(graph_label))
        self.wait()

Этот код демонстрирует создание графиков математических функций с подписями осей.

  • Axes: создаёт координатную сетку с заданными диапазонами (например, x от -3 до 3, y от -5 до 5) и размерами

  • get_axis_labels: добавляет подписи «x» и «f(x)» к осям координат

  • plot(lambda x: x**2): строит график математической функции (в данном случае x в квадрате) на координатных осях

  • get_graph_label: добавляет подпись с формулой функции рядом с построенной кривой

  • self.add: мгновенно отображает объекты на экране без анимации их появления

Если вам нужно получить изображение последнего кадра сцены, добавьте параметр -s к команде:

manim -p -qh -s more.py Graph

Вы также можете анимировать сам процесс построения осей:

manim -p -qh more.py Graph

Совместное перемещение объектов

Вы можете использовать VGroup, чтобы объединять разные объекты Manim и перемещать их как единое целое:

from manim import *

class TracePath(Scene):
    def construct(self):
        dot = Dot(color=RED)

        # Create traced path
        path = TracedPath(dot.get_center, stroke_color=BLUE, stroke_width=4)
        self.add(path, dot)

        # Move the dot in a circular pattern
        self.play(
            MoveAlongPath(dot, Circle(radius=2)),
            rate_func=linear,
            run_time=4
        )
        self.wait()

Этот код показывает, как группировать объекты и двигать их вместе как один элемент.

  • VGroup: объединяет несколько объектов, позволяя управлять ими одновременно (аналогично выделению нескольких элементов)

  • arrange(RIGHT, buff=1): выстраивает объекты горизонтально слева направо с расстоянием в 1 единицу между ними

  • shift(UP * 2): перемещает всю группу вверх на 2 единицы (умножение на 2 увеличивает расстояние перемещения вдвое)

Вы также можете создавать несколько групп и управлять ими независимо или совместно:

from manim import *

class GroupCircles(Scene):
    def construct(self):
        # Create circles
        circle_green = Circle(color=GREEN)
        circle_blue = Circle(color=BLUE)
        circle_red = Circle(color=RED)

        # Set initial positions
        circle_green.shift(LEFT)
        circle_blue.shift(RIGHT)

        # Create 2 different groups
        gr = VGroup(circle_green, circle_red)
        gr2 = VGroup(circle_blue)
        self.add(gr, gr2)
        self.wait()

        # Shift 2 groups down
        self.play((gr + gr2).animate.shift(DOWN))

        # Move only 1 group
        self.play(gr.animate.shift(RIGHT))
        self.play(gr.animate.shift(UP))

        # Shift 2 groups to the right
        self.play((gr + gr2).animate.shift(RIGHT))
        self.play(circle_red.animate.shift(RIGHT))
        self.wait()

Трассировка траектории

Вы можете использовать TracedPath, чтобы создать след движения объекта:

from manim import *

class TracePath(Scene):
    def construct(self):
        dot = Dot(color=RED)

        # Create traced path
        path = TracedPath(dot.get_center, stroke_color=BLUE, stroke_width=4)
        self.add(path, dot)

        # Move the dot in a circular pattern
        self.play(
            MoveAlongPath(dot, Circle(radius=2)),
            rate_func=linear,
            run_time=4
        )
        self.wait()

Этот код показывает, как создать след, который повторяет движение объекта.

  • Dot: маленькая круглая точка, которую можно перемещать

  • TracedPath(dot.get_center): создаёт линию, которая рисуется по мере движения точки, повторяя её траекторию (как след от пера)

  • get_center: возвращает текущую позицию точки, чтобы TracedPath знал, где рисовать

  • MoveAlongPath(dot, Circle(radius=2)): перемещает точку по круговой траектории радиуса 2

  • rate_func=linear: обеспечивает равномерную скорость движения (без ускорений и замедлений)

  • run_time=4: анимация длится 4 секунды

Для более сложного примера можно создать катящийся круг, который рисует циклоидальную кривую:

from manim import *

class RollingCircleTrace(Scene):
    def construct(self):
        # Create circle and dot
        circ = Circle(color=BLUE).shift(4*LEFT)
        dot = Dot(color=BLUE).move_to(circ.get_start())

        # Group dot and circle
        rolling_circle = VGroup(circ, dot)
        trace = TracedPath(circ.get_start)

        # Rotate the circle
        rolling_circle.add_updater(lambda m: m.rotate(-0.3))

        # Add trace and rolling circle to the scene
        self.add(trace, rolling_circle)

        # Shift the circle to 8*RIGHT
        self.play(rolling_circle.animate.shift(8*RIGHT), run_time=4, rate_func=linear)

Этот код создаёт анимацию катящегося колеса, которое рисует кривую по мере движения.

  • Настройка: создаётся синий круг слева и синяя точка, размещённая на краю круга

  • Группировка: VGroup объединяет круг и точку, чтобы они двигались вместе

  • Отслеживание пути: TracedPath следит за начальной точкой круга и рисует линию по её траектории

  • Непрерывное вращение: add_updater заставляет круг автоматически вращаться на каждом кадре, создавая эффект качения

  • Движение качения: при смещении группы вправо апдейтер вращения создаёт ощущение катящегося колеса

  • Циклоидальная кривая: трассируемый путь формирует волнообразную линию, показывающую математическую траекторию точки на катящемся колесе

Итоги

Поздравляем! Вы только что познакомились с Manim и его возможностями. Подведём итог: в Manim есть три основных типа сущностей:

  • Mobjects: объекты, которые можно отображать на экране, например Circle, Square, Matrix, Angle и другие

  • Scenes: «полотно» для анимаций, такие как Scene, MovingCameraScene и т. д.

  • Animations: анимации, применяемые к Mobjects, например Write, Create, GrowFromCenter, Transform и другие

Возможности Manim на этом далеко не заканчиваются — многое осталось за рамками этой статьи. Лучший способ разобраться глубже — практика, поэтому я рекомендую попробовать примеры из этой статьи и ознакомиться с туториалами Manim.

Русскоязычное сообщество про Python

Друзья! Эту статью подготовила команда Python for Devs — канала, где каждый день выходят самые свежие и полезные материалы о Python и его экосистеме. Подписывайтесь, чтобы ничего не пропустить!