Manim 里用 next_to 摆好的标签,物体一动为什么不跟着走?

2026 年 10 月 3 日 · 极坐标⋅XYZ

Manim 里用 next_to 摆好的标签,物体一动为什么不跟着走?

因为 next_to 只在你调用它的那一行算一次位置。它读一下参照物此刻在哪,把标签挪过去,事情就结束了,Manim 不会记下「标签要一直在三角形右边」这层关系。之后三角形再动,标签自然留在原地。想让标签一直跟着,要么给它挂一个 updater,每一帧重新贴一次;要么把两者装进一个 VGroup,当成一个整体来动。

Manim 是 3Blue1Brown 的作者 Grant Sanderson 开源的数学动画引擎。你用 Python 写清楚画面上有哪些图形和公式、它们怎么动,它负责把每一帧算出来,最后合成视频。现在大多数人用的是社区维护的 Manim Community 版,2026 年的最新版本是 0.21.0。这篇里的代码和坐标数字,都是 2026 年 10 月在 Manim 0.21.0 上跑出来的。想多了解一点 Manim,可以看这篇介绍。

下面三段代码可以在极坐标⋅XYZ 的工作区里直接运行,点代码块上面的「在工作区中打开」就行。工作区里装的也是 Manim 0.21.0,电脑上不用装 Python 和 LaTeX。

先把问题跑出来

一个三角形,右边用 next_to 摆一个 △ABC 的标签,然后把三角形往左挪 5 个单位:

from manim import *


class LabelLeftBehind(Scene):
    def construct(self):
        tri = Polygon([-2, -2, 0], [0, 2, 0], [3, 0, 0], color=BLUE)
        tri.set_fill(BLUE, 0.5).scale(0.5).move_to(RIGHT * 3)
        label = MathTex(r"\triangle ABC", color=YELLOW)
        label.next_to(tri, RIGHT, buff=0.3)

        self.add(tri, label)
        self.wait(0.5)
        self.play(tri.animate.shift(LEFT * 5), run_time=2)
        self.wait()

三角形走了,标签没动。我们把坐标打印出来看过:移动前三角形右边缘在 x = 4.25,标签左边缘在 x = 4.55,中间正好隔着 buff=0.3。移动后三角形右边缘到了 x = -0.75,标签左边缘还是 4.55,两者隔了 5.3 个单位。

Manim 里三角形执行 shift(LEFT * 5) 左移之后,用 next_to 摆好的 △ABC 标签仍留在原地:三角形右边缘到了 x = -0.75,标签左边缘还在 x = 4.55,间距从 0.3 变成 5.3

next_to 那一行到底做了什么

label.next_to(tri, RIGHT, buff=0.3) 执行时,Manim 做了三件事:取 tri 外接框右边缘的中点,取 label 外接框左边缘的中点,然后把 label 平移,让这两个点在水平方向上相距 0.3。

算完就是一次普通的平移,label 身上只留下了新坐标。tri 这个参照物没有被存到任何地方,Manim 里也没有「约束」这种东西在后台盯着。所以这行代码的效果和你手算出坐标再写 label.move_to([5.33, 0, 0]) 没有区别,只是省了你算的那一步。

align_to、to_edge、to_corner、move_to(另一个物体) 都是同一个脾气,摆的是调用那一刻的位置。

网上不少教程的示例里,物体动了标签也跟着动,看起来像是自动的。翻代码会发现,那是在物体挪到新位置之后又调了一次 next_to,或者干脆每帧都在调。

改法一:挂一个 updater,每帧重新贴一次

updater 是挂在物体上的一个函数,动画播放期间 Manim 每渲染一帧之前都会调它一遍。把 next_to 放进去,标签每一帧都按三角形的最新位置重新摆:

from manim import *


class LabelFollows(Scene):
    def construct(self):
        tri = Polygon([-2, -2, 0], [0, 2, 0], [3, 0, 0], color=BLUE)
        tri.set_fill(BLUE, 0.5).scale(0.5).move_to(RIGHT * 3)
        label = MathTex(r"\triangle ABC", color=YELLOW)
        label.next_to(tri, RIGHT, buff=0.3)
        label.add_updater(lambda m: m.next_to(tri, RIGHT, buff=0.3))

        self.add(tri, label)
        self.wait(0.5)
        self.play(tri.animate.shift(LEFT * 5), run_time=2)
        self.play(tri.animate.scale(1.6), run_time=1.5)
        self.play(Rotate(tri, PI / 2), run_time=1.5)

        label.clear_updaters()
        self.play(tri.animate.shift(UP * 1.5))
        self.wait()

比上一段只多了 add_updater 那一行。lambda m: ... 里的 m 就是 label 自己,Manim 调用时会传进来。

这段后面故意多做了几个动作。三角形放大 1.6 倍、再转 90°,标签始终贴在它右边 0.3 的位置,自己的大小没变。因为 next_to 每帧重新读的是三角形外接框的右边缘,三角形变大、变形、转向,外接框跟着变,标签也就跟着让位。

最后 label.clear_updaters() 把 updater 摘掉,之后三角形往上走,标签又不跟了。做动画时经常需要这样:前半段跟着,到某个时刻松手,让标签留在原地或者单独飞去别处。

前面那行 label.next_to(...) 不要删。add_updater 挂上去的时候默认不会先执行一次,而且紧跟着的 self.wait(0.5) 也不会触发它:wait 期间只有带 dt 参数的 updater(写成 lambda m, dt: ... 的那种)才会被调用,像这里 lambda m: ... 这样不带 dt 的,Manim 认为画面是静止的,直接跳过。我们试过把那行删掉,标签在开头的半秒里一直待在画面正中央,等三角形开始动才跳到它旁边。不想写两遍的话,可以用 label.add_updater(..., call_updater=True),挂上的同时立刻执行一次。

改法二:装进 VGroup,一起动

如果标签和图形从头到尾都是一体的,不需要中途分开,更省事的办法是打包:

from manim import *


class GroupMove(Scene):
    def construct(self):
        tri = Polygon([-2, -2, 0], [0, 2, 0], [3, 0, 0], color=BLUE)
        tri.set_fill(BLUE, 0.5).scale(0.5).move_to(RIGHT * 3)
        label = MathTex(r"\triangle ABC", color=YELLOW)
        label.next_to(tri, RIGHT, buff=0.3)
        group = VGroup(tri, label)

        self.add(group)
        self.wait(0.5)
        self.play(group.animate.shift(LEFT * 5), run_time=2)
        self.play(group.animate.scale(1.6), run_time=1.5)
        self.wait()

注意动画的对象换成了 group。平移时两者一起走,间距保持 0.3。

放大就能看出它和改法一的差别了。对 group 放大 1.6 倍,标签也跟着大了 1.6 倍,间距从 0.3 变成 0.48。整组是当一张图在缩放,里面的相对位置等比例变。

三角形放大 1.6 倍后的对比:用 add_updater 跟随时标签大小不变、间距仍是 0.3;装进 VGroup 一起放大时标签也放大 1.6 倍、间距变成 0.48

所以怎么选,看你要的是哪种「跟着」:

updater VGroup
平移 跟着走 跟着走
图形放大 标签大小不变,间距不变 标签一起放大,间距一起放大
图形旋转 标签不转,贴在新外接框旁边 标签一起转
中途分开 clear_updaters() 改回对单个物体做动画
每帧开销 多调一次函数 没有

给动点标字母、给会伸缩的线段标长度,一般用 updater。一个带文字的图标、一个整体移动的小组件,用 VGroup。

常见写错的地方

在循环里写 lambda,所有标签都跟着最后一个点

这个我们自己也踩过:

for dot, label in zip(dots, labels):
    label.add_updater(lambda m: m.next_to(dot, UP))

动画一播,三个标签全叠到最后一个点上面。原因在 Python,不在 Manim:lambda 里的 dot 是等到被调用时才去取值的,那时循环早跑完了,dot 已经是最后一个。用默认参数把当前这个 dot 留住就好:

for dot, label in zip(dots, labels):
    label.add_updater(lambda m, d=dot: m.next_to(d, UP))

在 for 循环里写 lambda m: m.next_to(dot, UP),A、B、C 三个标签全叠在最后一个点上;改成 lambda m, d=dot 后每个标签回到自己的点上方

挂了 updater 之后还想手动挪标签

label.shift(UP) 或者 self.play(label.animate.shift(UP)) 都留不住,下一帧 updater 又把它贴回三角形旁边。要挪就先 clear_updaters(),或者改 updater 里的方向和 buff。

只动了一次,却挂了 updater

如果三角形只在某一步挪一下,之后就不动了,不必上 updater,在同一个 play 里让标签也走过去就行:

self.play(
    tri.animate.shift(LEFT * 5),
    label.animate.shift(LEFT * 5),
)

常见追问(FAQ)

Q:always_redraw 和这里的 add_updater 有什么区别? A:always_redraw 每一帧把物体重新造一遍,适合形状本身要变的东西,比如连接两个动点的线段、会伸缩的花括号。标签的形状没变,只是换位置,用 add_updater 加 next_to 就够了,不用每帧重新排一次公式。

Q:updater 会拖慢渲染吗? A:一个 next_to 的计算量很小,几十个标签都感觉不出来。慢的是在 updater 里每帧新建 MathTex 这类重对象。

Q:self.add 之前挂 updater 和之后挂有区别吗? A:先后顺序没有影响。但物体必须在场景里,updater 才会被调用。只挂了 updater 却没 add 的物体,位置不会变。

Q:我想让标签一直在点的右上方,而且离得稍远一点? A:把方向换成 UR,buff 调大:m.next_to(dot, UR, buff=0.2)。方向常量可以随便换,UP、DL 都行。


这个知识点在教程《布局(上)· 描述关系不算坐标》的最后一节有视频讲解,旁边的代码可以边看边改;updater 的完整用法在《Updater》那一课。

来源:极坐标⋅XYZ · jizuobiao.xyz · 更新于 2026-10-03 · 基于 Manim Community 0.21.0