ZhangZhihui's Blog  

 

from manim import *

class MovingAngle(Scene):
    def construct(self):
        rotation_center = LEFT

        theta_tracker = ValueTracker(110)
        line1 = Line(LEFT, RIGHT)
        line_moving = Line(LEFT, RIGHT)
        line_ref = line_moving.copy()
        line_moving.rotate(
            theta_tracker.get_value() * DEGREES, about_point=rotation_center
        )
        a = Angle(line1, line_moving, radius=0.5, other_angle=False)
        tex = MathTex(r"\theta").move_to(
            Angle(
                line1, line_moving, radius=0.5 + 3 * SMALL_BUFF, other_angle=False
            ).point_from_proportion(0.5)
        )

        self.add(line1, line_moving, a, tex)
        self.wait()

        line_moving.add_updater(
            lambda x: x.become(line_ref.copy()).rotate(
                theta_tracker.get_value() * DEGREES, about_point=rotation_center
            )
        )

        a.add_updater(
            lambda x: x.become(Angle(line1, line_moving, radius=0.5, other_angle=False))
        )
        tex.add_updater(
            lambda x: x.move_to(
                Angle(
                    line1, line_moving, radius=0.5 + 3 * SMALL_BUFF, other_angle=False
                ).point_from_proportion(0.5)
            )
        )

        self.play(theta_tracker.animate.set_value(40))
        self.play(theta_tracker.animate.increment_value(140))
        self.play(tex.animate.set_color(RED), run_time=0.5)
        self.play(theta_tracker.animate.set_value(350))

Angle(line1, line_moving, radius=0.5, other_angle=False) 中的参数other_angle是什么意思?

 

在 Manim 的 Angle 类中,other_angle 参数决定了当两条线相交时,大括号(弧线)绘制在哪一个角度上。

1. 直观理解

当两条直线相交时,它们之间实际上形成了四个角(两对对顶角)。即便我们只看相邻的两个角,它们加起来也是 180°。

  • other_angle=False (默认值):绘制“常规”的那个角。通常是根据两条线的向量方向确定的主角。

  • other_angle=True:绘制该角的补角(即 180° 减去当前角的那一部分)。


2. 为什么需要它?

在数学动画中,你可能想标注两条线交点的“左侧角”或者“右侧角”。由于两条线的定义顺序和方向可能导致 Manim 默认选择了你不想要的那个角,other_angle 就成了一个快速切换开关。

3. 代码中的逻辑

在你的代码里:

Python
 
a = Angle(line1, line_moving, radius=0.5, other_angle=False)

如果 line1 是指向右的,line_moving 是向上旋转 110° 的:

  • False:你会得到一个 110° 的角。

  • True:你会得到它旁边的那个角(即 $180^\circ - 110^\circ = 70^\circ$ 的那个角)。


4. 特别注意:它不是“优角”

很多初学者会把 other_angle 误认为是切换优角 (Reflex Angle)(即大于 180° 的那个角)。

  • 如果你想要显示大于 180° 的角(比如 250°),你需要调整线段的定义方向,或者使用 radius 为负值等技巧。

  • other_angle 主要是用来在相邻的两个补角之间做切换。

5. 你的代码为什么要用它?

在你的 MovingAngle 场景中,theta_tracker 的值会一直变到 350°。

  • 当角度超过 180° 时,Angle 的行为可能会变得违反直觉。

  • 通过显式设置 other_angle=False,你确保了 Manim 始终尝试按照你传入的两条线的拓扑关系来绘制那个“主角度”,从而配合 add_updater 实现平滑的动画效果。

总结一句话:

other_angle 就是用来切换到隔壁那个补角的开关。如果默认出来的弧线位置不对,把它改成 True 往往就能解决问题。

 

 

Angle(line1, line_moving, radius=0.5, other_angle=False)中的参数radius是控制什么的?

 

在 Angle 类中,radius 参数控制的是表示角度的那段弧线的半径大小。

简单来说,它决定了那个弧线离顶点(两条线的交点)有多远。

1. 核心作用

  • 视觉距离:radius 值越大,弧线就画得越大,离两条线的交点越远。

  • 默认值:如果不指定,它的默认值通常是 0.5(Manim 单位)。

2. 在你代码中的具体体现

在你的代码里,radius 出现了两次,起到了不同的作用:

A. 绘制主弧线

Python
 
a = Angle(line1, line_moving, radius=0.5, other_angle=False)

这里 radius=0.5 设定了你看到的那个黄色(或默认色)圆弧的大小。

B. 定位标签($\theta$)

Python
 
tex = MathTex(r"\theta").move_to(
    Angle(
        line1, line_moving, radius=0.5 + 3 * SMALL_BUFF, other_angle=False
    ).point_from_proportion(0.5)
)

这是一个非常聪明的用法!为了让符号 $\theta$ 准确地放在弧线外面一点点,你创建了一个虚拟的、半径更大的 Angle:

  • 这个虚拟角度的半径是 0.5 + 3 * SMALL_BUFF。

  • 通过 point_from_proportion(0.5) 找到这段大弧线的中点。

  • 将文本 $\theta$ 移动到这个位置,从而实现了“标签始终跟随角度旋转并保持间距”的效果。


3. radius 的特殊技巧

  • 正值与负值:

    • 通常使用正值。

    • 但在某些复杂的几何情况下,将 radius 设为负值,可以强制让弧线转到交点的另一侧(起到类似对顶角的效果),这在某些版本的 Manim 中是一种实用的调试技巧。

  • 配合 SMALL_BUFF:

    • 在定位文本时,通常会在 radius 上加一个微小的偏移量(Buffer),防止文字直接压在弧线上。

总结:

radius 就是用来调节角度弧线“个头”大小的参数。个头越大,标注就越明显,但也越占地方。

 

 

lambda x: x.become(line_ref.copy())

become()是用来干什么的?

 

在 Manim 中,become() 是一个非常强大且特殊的方法。简单一句话概括:它能让一个物体“变身”成另一个物体的样子,但保持自己的身份不变。


1. 核心作用:状态替换

当你调用 A.become(B) 时:

  • 外观克隆:A 会立即获得 B 的所有视觉属性,包括位置、颜色、形状、大小、旋转角度等。

  • 身份保留:A 依然是原来的那个 Python 对象(内存地址没变)。这意味着之前所有绑定在 A 上的引用(比如它在某个 Group 里,或者有其他的 updater)依然有效。


2. 为什么在 Updater 里必须用 become()?

在你提供的代码中:

Python
 
line_moving.add_updater(
    lambda x: x.become(line_ref.copy()).rotate(...)
)

这是 become() 最经典的使用场景。

如果不使用 become() 的后果:

如果你直接在 updater 里写 x.rotate(...),由于 updater 是每一帧都在执行的,line_moving 会在上一帧已经旋转的基础上叠加旋转。结果就是线条会像风扇一样越转越快,最后飞出屏幕。

使用 become() 的逻辑:

  1. 重置:x.become(line_ref.copy())。每一帧开始时,先把 line_moving 变回最初那根水平线的样子(line_ref)。

  2. 重新计算:然后再根据 theta_tracker 当前的最新的值旋转一次。

  3. 循环:下一帧重复这个过程——先重置,再旋转。

这样就确保了物体的状态始终与 ValueTracker 的数值绝对同步。


3. become() vs 直接赋值

  • 直接赋值 (x = B):这只是把变量名指向了新对象。Manim 的渲染引擎并不知道你换了对象,原本在屏幕上的 A 不会发生任何变化。

  • 使用 become():它是“整容手术”。物体 A 还在屏幕上,但它的每一条路径、每一个像素都变成了 B 的样子。


4. 常见使用场景

场景 描述
复杂动画同步 像你代码中那样,让一个复杂的几何图形(如 Angle)随变量实时刷新。
物体“变身” 在动画中让一个圆圈无缝变成一个方块(虽然 ReplacementTransform 也能做,但 become 在 updater 中更受控)。
状态回滚 快速将物体恢复到之前保存的某个快照状态。

5. 关于 .copy()

在你的代码里写的是 x.become(line_ref.copy())。

  • 为什么要 copy? 因为 become 会修改目标对象的内部数据。为了保护原件 line_ref 不被意外修改,通常先克隆一份副本作为“样板”给 become 使用。

总结:

become() 是 Manim 实现动态响应式动画的基石。它解决了一个核心问题:如何在保持物体身份连续性的同时,每一帧都重新定义它的样子。

 

posted on 2026-05-15 16:29  ZhangZhihuiAAA  阅读(29)  评论(0)    收藏  举报