Manim 的 shift 和 move_to 有什么区别,什么时候用哪个?

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

Manim 的 shift 和 move_to 有什么区别,什么时候用哪个?

shift 的参数是一段位移,物体从现在的位置出发,按这个方向和距离挪过去。move_to 的参数是一个落点,物体的中心直接放到那个点上,原来在哪都一样。所以同一行 shift 多执行几次,物体会越走越远;move_to 执行多少次都停在同一处。知道终点坐标、或者要对到另一个物体上,用 move_to;只想在现有位置上挪一点,用 shift。

Manim 是一个用 Python 写数学动画的开源引擎,最早是 Grant Sanderson 给自己的 3Blue1Brown 频道写的。图形、公式、坐标系在代码里都是对象,摆在哪、怎么动由你一行行写出来,渲染时它逐帧算好再合成视频。现在通用的是社区维护的 Manim Community,2026 年的最新版本是 0.21.0,本文的代码、坐标和报错信息都是 2026 年 10 月在这一版上跑出来的。没接触过 Manim 的话,可以先看这篇介绍。

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

同一个 RIGHT * 2,两个结果

两个方块叠在一起,都从 (-3, 1) 出发。蓝的用 shift,黄的用 move_to,传进去的参数一模一样,都是 RIGHT * 2。同样的两行连播两遍:

from manim import *


class ShiftVsMoveTo(Scene):
    def construct(self):
        plane = NumberPlane(background_line_style={"stroke_opacity": 0.3})
        start = Dot([-3, 1, 0], color=GREY)

        a = Square(side_length=1, color=BLUE).set_fill(BLUE, 0.5).move_to([-3, 1, 0])
        b = Square(side_length=1, color=YELLOW).set_fill(YELLOW, 0.5).move_to([-3, 1, 0])
        self.add(plane, start, a, b)
        self.wait(0.5)

        # 参数一模一样,都是 RIGHT * 2
        self.play(a.animate.shift(RIGHT * 2), b.animate.move_to(RIGHT * 2), run_time=2)
        self.wait(0.5)

        # 同样的两行再来一遍
        self.play(a.animate.shift(RIGHT * 2), b.animate.move_to(RIGHT * 2), run_time=2)
        self.wait()

第一遍播完,蓝方块的中心在 (-1, 1),黄方块的中心在 (2, 0)。

RIGHT * 2 在 Manim 里就是数组 [2, 0, 0]。shift 把它当成「往右 2」,蓝方块的 x 从 -3 变成 -1,y 没动。move_to 把同一个数组当成坐标 (2, 0),黄方块的中心直接落过去,连 y 也从 1 变成了 0。

Manim 里两个方块都从 (-3, 1) 出发,参数同样是 RIGHT * 2:shift 把方块往右挪 2 个单位,中心到 (-1, 1);move_to 把方块中心直接放到点 (2, 0)

第二遍,蓝方块又往右走了 2,到了 (1, 1)。黄方块已经在 (2, 0) 了,这一遍它的动画里什么也看不到。

我们另外单独试过连调三次:shift(RIGHT * 2) 的 x 是 -1、1、3,move_to([2, 1, 0]) 三次都是 (2, 1)。

同一行代码连调三次的对比:shift(RIGHT * 2) 每次在上一次基础上再走 2,x 从 -1 到 1 再到 3;move_to([2, 1, 0]) 三次都停在 x = 2

move_to 其实是用 shift 写的

翻 Manim 0.21.0 的源码,move_to 的函数体只有几行,去掉类型标注大致是这样:

def move_to(self, point_or_mobject, aligned_edge=ORIGIN, coor_mask=np.array([1, 1, 1])):
    if isinstance(point_or_mobject, Mobject):
        target = point_or_mobject.get_critical_point(aligned_edge)
    else:
        target = point_or_mobject
    point_to_align = self.get_critical_point(aligned_edge)
    self.shift((target - point_to_align) * coor_mask)
    return self

它先算出目标点,再取自己身上的对齐点,两者一减就是要走的位移,最后把这段位移交给 shift。真正改动物体的只有 shift,它做的事是给物体的每个点都加上同一个向量。move_to 多出来的那一步,是替你把「到那儿还差多远」算了。

从这几行还能读出三件事。

对齐点默认是外接框的中心。aligned_edge 的默认值是 ORIGIN,get_critical_point(ORIGIN) 取到的是物体外接框正中间那个点,也就是 get_center()。它不一定是你直觉里的「中心」:顶点在 (-2, -2)、(0, 2)、(3, 0) 的三角形,外接框中心是 (0.5, 0),重心是 (0.33, 0)。对它 move_to(ORIGIN),落到原点上的是前者。

落点可以是另一个物体。传进来的是 Mobject 时,目标点取的是那个物体的对齐点,默认也是中心,所以 tri.move_to(circle) 的结果是两个中心重合。我们把一个三角形 move_to 到圆心在 (-2, -1) 的圆上,打印出来的中心就是 (-2, -1)。

aligned_edge 可以换对齐点。写成 LEFT,两边取的都是左边缘的中点;写成 UL,取的都是左上角。

from manim import *


class MoveToTargets(Scene):
    def construct(self):
        plane = NumberPlane(background_line_style={"stroke_opacity": 0.3})
        box = Rectangle(width=6, height=3, color=GREY_B).move_to([1, 0.5, 0])
        sq = Square(side_length=1, color=YELLOW).set_fill(YELLOW, 0.5).move_to([-5, -3, 0])
        self.add(plane, box, sq)
        self.wait(0.5)

        # 落点是一个物体:中心对中心
        self.play(sq.animate.move_to(box))
        self.wait(0.5)
        # 加上 aligned_edge:左边对左边
        self.play(sq.animate.move_to(box, aligned_edge=LEFT))
        self.wait(0.5)
        # 左上角对左上角
        self.play(sq.animate.move_to(box, aligned_edge=UL))
        self.wait(0.5)
        # 落点是坐标时,aligned_edge 指的是「自己的哪个点落上去」
        self.play(sq.animate.move_to(ORIGIN, aligned_edge=DL))
        self.wait()

最后一步,方块的左下角落在原点,中心在 (0.5, 0.5)。

Manim 的 move_to 落点是另一个物体时的三种对齐:默认中心对中心,aligned_edge=LEFT 左边中点对左边中点,aligned_edge=UL 左上角对左上角

什么时候用哪个

你想做的事 用哪个 写法
放到画面上一个确定的位置 move_to sq.move_to([2, 1, 0])
回到画面正中 move_to sq.move_to(ORIGIN)
叠到另一个物体上,或者对齐它的某条边 move_to sq.move_to(box, aligned_edge=LEFT)
在现在的位置上挪一点 shift sq.shift(UP * 0.5)
摆好之后微调 shift label.next_to(dot, UP).shift(RIGHT * 0.1)
一组物体整体平移,内部布局不变 两个都行 group.shift(LEFT * 3) 或 group.move_to(ORIGIN)

我们自己写场景时的习惯是:物体第一次摆位置用 move_to 或者 next_to,之后的动作多半是 shift。这样改起点只动一行,后面的位移都跟着走。

最后一行提一句。对 VGroup 调 move_to,对齐的是整组的外接框中心,成员一起平移,相互位置不变。我们试过一个方块在 (-2, 0)、一个圆在 (2, 1) 的组,group.move_to(ORIGIN) 之后两者到了 (-2, -0.5) 和 (2, 0.5)。要是写成循环,对每个成员单独 move_to(ORIGIN),它们就全叠到原点上了。

常见写错的地方

在 updater 里写 shift

updater 每一帧都会被调用,shift 又是累加的,写在一起物体就一直走:

from manim import *


class ShiftInUpdater(Scene):
    def construct(self):
        plane = NumberPlane(background_line_style={"stroke_opacity": 0.3})
        per_frame = Dot([-6, 1, 0], color=RED)
        per_second = Dot([-6, 0, 0], color=GREEN)
        pinned = Dot([-6, -1, 0], color=YELLOW)

        per_frame.add_updater(lambda m: m.shift(RIGHT * 0.1))        # 每帧走 0.1
        per_second.add_updater(lambda m, dt: m.shift(RIGHT * 2 * dt))  # 每秒走 2
        pinned.add_updater(lambda m: m.move_to([-4, -1, 0]))         # 每帧都摆回同一个点

        self.add(plane, per_frame, per_second, pinned)
        self.wait(2)

红点每帧走 0.1,走多远取决于帧率。我们用 -ql(15 帧)和 -qh(60 帧)各渲了一遍:2 秒后红点分别在 x = -2.8 和 x = 6.2,一个走了 3.2,一个走了 12.2。预览时看着正常,换成高清导出,点就快冲出画面了。

绿点的 updater 多收一个 dt,是上一帧到这一帧过了多少秒。位移乘上它,速度就是「每秒 2」,两种帧率下 2 秒后都在 x = -2 附近。想让物体匀速一直走,用这种写法。

黄点的 updater 里是 move_to,每帧摆到同一个点,它从头到尾待在 (-4, -1)。

顺带说一句,红点和黄点的 updater 不带 dt,它们在 wait 期间能动,是因为场景里有绿点那个带 dt 的 updater。我们试过把绿点那行删掉,wait(2) 之后红点还在 x = -6,黄点也没去 (-4, -1):场景里一个带 dt 的 updater 都没有时,Manim 把 wait 当成静止画面,不调 updater。这一点上一篇讲过。

上一篇讲标签跟随时在 updater 里放的 next_to 也是这个道理:位置每帧重算一遍,重算多少次都不会跑。

同一个 play 里给同一个物体写两个 .animate

想让点往右 2 的同时往上 2,这样写不行:

self.play(dot.animate.shift(RIGHT * 2), dot.animate.shift(UP * 2))

我们试下来,点最后在 (0, 2),只往上走了,往右那一个被后面的盖掉了。同一个物体的多个动作要串在一条 .animate 后面:

self.play(dot.animate.shift(RIGHT * 2).shift(UP * 2))
# 或者直接把向量加起来
self.play(dot.animate.shift(RIGHT * 2 + UP * 2))

串起来之后,.animate 是按最终位置做一次插值,点走的是从起点到终点的直线,不会先右后上拐个弯。shift 和 move_to 也可以混着串,比如 dot.animate.move_to([2, 1, 0]).shift(LEFT),终点是 (1, 1)。

move_to 的坐标少写了 z

sq.move_to([2, 1])

在 0.21.0 上这行直接报 ValueError: operands could not be broadcast together with shapes (2,) (3,)。Manim 里的点都是三维的,二维场景也要把 z 写上:[2, 1, 0]。shift([2, 1]) 报的是同一类错。写成 RIGHT * 2 + UP 可以绕开这个问题,方向常量本身就是三个数。

以为 move_to(另一个物体) 之后会一直跟着

a.move_to(b) 读的是 b 在这一行执行时的位置。我们试过在同一个 play 里让 a 去 move_to(b)、同时让 b 往上走 2:播完 a 停在 (3, 0),那是 b 出发前的位置,b 已经到了 (3, 2)。要让 a 一直贴着 b,得用 updater 每帧重摆,做法在上一篇里。

常见追问(FAQ)

Q:只想改 x 坐标,y 不动,用哪个? A:sq.set_x(1)。也可以写 sq.move_to([1, -1, 0], coor_mask=[1, 0, 0]),coor_mask 里为 0 的轴不动。我们把一个中心在 (-3, 2) 的物体这样移,两种写法得到的都是 (1, 2)。只改 y 用 set_y。

Q:shift 能一次传几个方向吗? A:能,sq.shift(RIGHT * 2, UP) 会把几个向量加起来,等于 sq.shift(RIGHT * 2 + UP)。

Q:shift(LEFT * 3) 里的 3 是多少像素? A:是 Manim 的画布单位,和像素没有固定关系。默认画布高 8 个单位、宽约 14.2 个单位,渲染成 1080p 还是 4K,物体在画面里的相对位置都一样。

Q:move_to 和 next_to 怎么选? A:move_to 是让两个物体的同一个点重合,结果是叠在一起或者贴着同一条边。next_to 是摆在另一个物体的外侧,中间留一段 buff。给图形配标签用 next_to,把高亮框套到某个物体上用 move_to。

Q:.animate.move_to(...) 走的是直线吗? A:是。.animate 在起点和终点之间做直线插值,前面那个先右 2 再上 2 的链式写法,我们量过途中各帧,点一直在起点和终点的连线上。想沿一条指定的曲线走,用 MoveAlongPath。


shift 和 move_to 在教程《布局(上)· 描述关系不算坐标》的第一节有视频讲解,旁边的代码可以边看边改;.animate 和 updater 分别在《动画》和《ValueTracker 与 Updater》两课里。

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