Manim制作动画之Animation详解

肝了一上午, 终于写完了...

上一篇讲解了Mobject, 表示数学图形, 这一篇来讲讲Animation, 它表示动画, 是让图形动起来的关键. 另外, 文末分享清华大学DeepSeek资料.

简单的说, 动画就是两个Mobject之间插值的过程, 通过插入过渡效果的帧让动画变得平滑.

举🌰说明:

  • FadeIn: Mobject从完全透明到完全不透明, 中间插值逐渐改变透明度.
  • Rotate: 在初始状态和最终状态插值逐渐改变旋转角度.

.animate写法

只要一个Mobject的方法可以改变它的属性, 就可以通过.animate的写法将其变成动画.

示例代码:

from manim import *


class AnimateExample(Scene):
    def construct(self):
        square = Square().set_fill(WHITE, opacity=1.0)
        self.add(square)
        self.wait(1)

        # 在一秒内逐渐填充颜色
        self.play(square.animate.set_fill(RED), run_time=1)
        self.wait(1)

        # 在一秒内逐渐向上移动, 同时旋转60度
        self.play(square.animate.shift(UP).rotate(PI / 3), run_time=1)
        self.wait(1)

动画:

默认情况下play()方法渲染的动画会持续1秒钟, 使用run_time参数来改变持续时间.

自定义动画

尽管Manim有许多内置动画, 但通常这些动画都比较简单, 有时无法满足我们的需求. 这时就需要自定义动画.

要自定义动画, 就需要继承Animation类并重写interpolate_mobject()方法, 该方法有一个alpha参数, 这是控制动画的关键, 动画开始时alpha为0, 动画结束时alpha为1, 在这个过程中alpha逐渐增加. 我们要做的就是在interpolate_mobject()方法中根据alpha控制Mobject的状态.

来看一个示例, 体会一下这个alpha到底是什么意思.

示例: 倒计时5秒钟

from manim import *


class CountDown(Animation):
    # 第一个参数就是要操作的Mobject, 类型是DecimalNumber
    # start和end分别表示开始时间和结束时间
    def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
        # 向父类传递要操作的Mobject对象
        super().__init__(number, **kwargs)
        self.start = start
        self.end = end

    def interpolate_mobject(self, alpha: float) -> None:
        # 根据alpha设置要显示的值
        value = (self.end - self.start) * alpha + self.start
        self.mobject.set_value(value)


class CountingScene(Scene):
    def construct(self):
        # 创建一个DecimalNumber对象, 白色, 5倍大小
        number = DecimalNumber(5).set_color(WHITE).scale(5)
        self.add(number)
        self.wait()

        # rate_func=linear表示动画的速度是线性的, 即数字会均匀地从5减少到0
        self.play(CountDown(number, 5, 0), run_time=5, rate_func=linear)
        self.wait()

interpolate_mobject()方法中做到就是根据alpha值得到DecimalNumber对象要显示的值.

已知alpha=0时value=start, alpha=1时value=end, 将value看成是alpha的函数: value=k * alpha + b, 带入条件求出k和b, 就得出了15行的表达式.

在调用play()方法渲染动画时, 传入参数run_time=5, rate_func=linear, 表示在5秒内均匀地将数字从5减小到0.

动画:

如果报错, 排查一下自己有没有安装LaTex.

下载链接: Getting MiKTeX

示例: 沿着弧线交换位置

from manim import *
import numpy as np


class SwapAlongCircle(Animation):
    def __init__(self, mobject1, mobject2, **kwargs):
        self.mobject1 = mobject1
        self.mobject2 = mobject2
        # 计算圆心和半径, get_center()返回类型是ndarray, [x,y,z]表示三维空间的坐标
        self.center = (mobject1.get_center() + mobject2.get_center()) / 2
        self.radius = np.linalg.norm(mobject1.get_center() - mobject2.get_center()) / 2
        # 计算初始角度
        delta = mobject1.get_center() - mobject2.get_center()
        self.initial_angle = np.arctan2(delta[1], delta[0])  # 初始角度
        # 随便传一个Mobject给父类
        super().__init__(mobject1, **kwargs)

    def interpolate_mobject(self, alpha: float) -> None:
        # 计算当前角度
        angle = self.initial_angle + alpha * PI

        # 计算两个Mobject的当前位置, 两个Mobject都沿着逆时针方向移动
        pos1 = self.center + self.radius * np.array([np.cos(angle), np.sin(angle), 0])
        pos2 = self.center - self.radius * np.array([np.cos(angle), np.sin(angle), 0])

        # 更新两个Mobject的位置
        self.mobject1.move_to(pos1)
        self.mobject2.move_to(pos2)


class SwapAlongCircleScene(Scene):
    def construct(self):
        square1 = Square().set_fill(BLUE, opacity=1).shift(UP * 2 + LEFT)
        square2 = Square().set_fill(RED, opacity=1).shift(DOWN * 2 + RIGHT)

        self.add(square1, square2)
        self.wait(1)

        self.play(SwapAlongCircle(square1, square2), run_time=3, rate_functions=linear)
        self.wait(1)

这里主要是计算角度随alpha的变化, 然后配合三角函数算出两个Mobject的位置. (还需要点数学功底👻!)

另外, ndarray支持高效的数值计算, 可以避免循环. 合理的运用可以简化代码.

动画:

Mobject中的坐标

之前的示例只用过Mobject的get_center()方法, 它表示Mobject的中心, 除此之外, 还有许多其他方法用于返回Mobject中特定点的坐标. 这里一并列举:

  • get_center(): 中心点
  • get_top(): 上方的点
  • get_bottom: 下方的点
  • get_start(): 边界的起点
  • get_end(): 边界的终点
  • point_from_proportion(): 从边界的起点到终点的路径上指定比例的点

示例代码:

from manim import *


class MobjectExample(Scene):
    def construct(self):
        p1 = [-2, -1, 0]
        p2 = [-1, -1, 0]
        p3 = [1, 1, 0]
        p4 = [2, 1, 0]
        a = Line(p1, p2).append_points(Line(p2, p3).points).append_points(Line(p3, p4).points)
        point_start = a.get_start()
        point_end = a.get_end()
        point_center = a.get_center()
        # to_edge(UR)表示将对象的边缘对齐到屏幕右上角, 距离为默认的0.5
        self.add(Text(f"a.get_start() = {np.round(point_start, 2).tolist()}", font_size=24).to_edge(UR).set_color(YELLOW))
        # self.mobjects[-1]表示Scene中添加的最后一个对象
        self.add(Text(f"a.get_end() = {np.round(point_end, 2).tolist()}", font_size=24).next_to(self.mobjects[-1], DOWN).set_color(RED))
        self.add(Text(f"a.get_center() = {np.round(point_center, 2).tolist()}", font_size=24).next_to(self.mobjects[-1], DOWN).set_color(BLUE))

        self.add(Dot(a.get_start()).set_color(YELLOW).scale(2))
        self.add(Dot(a.get_end()).set_color(RED).scale(2))
        self.add(Dot(a.get_top()).set_color(GREEN_A).scale(2))
        self.add(Dot(a.get_bottom()).set_color(GREEN_D).scale(2))
        self.add(Dot(a.get_center()).set_color(BLUE).scale(2))

        # a.point_from_proportion(0.5)表示路径上的中点
        self.add(Dot(a.point_from_proportion(0.5)).set_color(ORANGE).scale(2))
        # 使用Python的列表解析并解包
        self.add(*[Dot(x) for x in a.points])
        self.add(a)

to_edge(UR)表示将Mobject边缘对齐到屏幕右上角, 距离为默认的0.5. 如果添加参数buff=0则表示Mobject边缘贴紧屏幕右上角.

self.add(*[Dot(x) for x in a.points]) 这一行表示使用a中的点创建Dot对象, 使用*将列表解包成一个个单独的对象.

动画:

奇怪的动画

Mobject可以看作是一个个点组成的图形, 所以在Transorm转换的时候Manim就会将一个个点转换到对应的位置. 正是由于这种机制, 导致可能出现奇怪的动画.

示例代码:

from manim import *


class MobjectExample(Scene):
    def construct(self):
        square1 = Square()
        square2 = Square().shift(RIGHT).rotate(PI)
        self.play(Transform(square1, square2), run_time=2)

动画:

解决方法: 使用numpy的roll()方法将Mobject中的点重新排列.

from manim import *


class MobjectExample(Scene):
    def construct(self):
        square1 = Square()
        # 逆时针旋转180度
        square2 = Square().shift(RIGHT).rotate(PI)

        points = square1.points
        print(points)

        # 顺时针旋转180度
        square2.points = np.roll(points, int(len(points) / 2), axis=0)

        self.play(Transform(square1, square2), run_time=2)

动画:

同样的道理, 可以让下面示例中变换具有对称效果.

from manim import *


class ExampleRotation(Scene):
    def construct(self):
        square1 = Square().set_color(RED).shift(LEFT)
        circle1 = Circle().set_color(RED).shift(LEFT)

        square2 = Square().set_color(BLUE).shift(RIGHT)
        circle2 = Circle().set_color(BLUE).shift(RIGHT)

        points = square2.points
        #  顺时针旋转90度
        points = np.roll(points, int(len(points) / 4), axis=0)
        square2.points = points

        self.play(Transform(square1, circle1), Transform(square2, circle2), run_time=1)

动画:

以上就是本篇的全部内容了!

最后再分享一份清华大学DeepSeek的资料, 公众号:算法铁金库, 输入暗号: 106. 有需要的同学自取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值