Manim 的画布到底多大?frame_width、单位和像素怎么换算
2026 年 10 月 14 日 · 极坐标⋅XYZ
Manim 的画布到底多大?frame_width、单位和像素怎么换算
Manim 默认的画布宽 14.22、高 8,单位是 Manim 自己的「单位长度」,不是像素。原点在正中间,所以 x 从 -7.11 到 7.11,y 从 -4 到 4。单位和像素之间只差一个系数:每单位的像素数 = pixel_height ÷ frame_height,1080p 下是 135,720p 是 90,480p 是 60。换画质只改像素,不改单位,所以 -ql 预览和 -qh 成片的构图完全一样。想让画布本身变大变小,要改 frame_width,并且把 frame_height 按同样的比例一起写上。
Manim 是 3Blue1Brown 的作者 Grant Sanderson 写的数学动画引擎,画面里每个图形、公式都是 Python 对象,位置和大小全用代码里的数字来定。这些数字的单位就是本文要讲的「单位长度」。现在大家用的是社区维护的 Manim Community,工作区装的是 0.21.0,2026 年 10 月 8 日又发布了 0.22.0,本文的数字我们在这两个版本上各跑了一遍,结果一致。没接触过 Manim 可以先看这篇介绍;画布和坐标这一课,极坐标⋅XYZ 上 2026 年最新的 Manim 中文教程里有视频讲解。
下面两段完整代码可以在极坐标⋅XYZ 的工作区里直接运行,点代码块上面的「在工作区中打开」就行,电脑上不用装 Python 和 LaTeX。
先把默认画布画出来
NumberPlane() 不加参数时正好铺满默认画布,每一小格是 1 个单位。在原点右上方放一个 1 × 1 的方块,再把画布右上角的坐标和当前的像素换算印在画面上:
from manim import *
class CanvasSize(Scene):
def construct(self):
# NumberPlane 默认铺满整个画布,每一小格是 1 个单位
self.add(NumberPlane())
# 原点右上方的那一格,正好 1 × 1
unit = Square(side_length=1, color=YELLOW).set_fill(YELLOW, 0.6)
unit.move_to([0.5, 0.5, 0])
self.play(FadeIn(unit))
# 画布的右上角:x 是 frame_width 的一半,y 是 frame_height 的一半
corner = config.right_side + config.top
dot = Dot(corner, color=RED, radius=0.12)
tag = Text(f"({corner[0]:.2f}, {corner[1]:g})", font_size=28, color=RED)
tag.next_to(dot, DL, buff=0.1)
self.play(FadeIn(dot), Write(tag))
px = config.pixel_height / config.frame_height # 每个单位多少像素
info = Text(
f"画布 {config.frame_width:.2f} × {config.frame_height:g} 单位 "
f"输出 {config.pixel_width} × {config.pixel_height} 像素 "
f"1 单位 = {px:.4g} 像素",
font_size=26,
)
info.to_edge(DOWN).add_background_rectangle(opacity=0.85, buff=0.15)
self.play(Write(info))
self.wait()
在本机用 manim -qh 渲,最后一帧底下印的是「画布 14.22 × 8 单位,输出 1920 × 1080 像素,1 单位 = 135 像素」,横向正好 14 格多一点,纵向 8 格。我们数了那个黄方块,宽 134 个实色像素,边上再加一圈半透明的抗锯齿,和 135 对得上。
在工作区里跑,默认预览是 480p,印出来是 854 × 480 像素、1 单位 = 60 像素,网格和方块的位置一点没变。

14.22 这个数是算出来的。Manim 把画布高度定成 8,宽度按 16:9 推:8 × 16 ÷ 9 = 14.222…,也就是 14 又 2/9。所以默认画布两侧各多出 1/9 格,NumberPlane 最外面那一列格子是窄的。
单位和像素怎么换算
画面上的东西用单位来摆,导出视频时 Manim 再乘一个系数变成像素。这个系数取决于你选的画质:
| 画质参数 | 输出像素 | 帧率 | 1 单位 = |
|---|---|---|---|
-ql |
854 × 480 | 15 | 60 像素 |
-qm |
1280 × 720 | 30 | 90 像素 |
-qh(不写时的默认值) |
1920 × 1080 | 60 | 135 像素 |
-qp |
2560 × 1440 | 60 | 180 像素 |
-qk |
3840 × 2160 | 60 | 270 像素 |
这张表是在 0.21.0 里直接读 manim.constants.QUALITIES 打出来的。换算的写法:
px_per_unit = config.pixel_height / config.frame_height # 1080p 下是 135
units = 100 / px_per_unit # 100 像素 ≈ 0.74 个单位
-ql 的 854 不是 480 × 16 ÷ 9 的整倍数(那是 853.33),所以 480p 下横向是每单位 60.05 像素,比纵向多一点点,肉眼看不出来。
换分辨率,构图为什么不变
因为画面里所有的位置和尺寸都是按单位记的。Square(side_length=1) 在什么画质下都是 1 个单位,渲出来是几个像素才跟着画质走。我们把同一个方块按四种分辨率各渲了一帧:
| 输出 | 1 × 1 方块渲出来 |
|---|---|
| 854 × 480 | 60 × 60 像素 |
| 1280 × 720 | 90 × 90 像素 |
| 1920 × 1080 | 134 × 134 像素(加抗锯齿的半透明边约 136) |
| 3840 × 2160 | 270 × 270 像素 |
文字也一样。Text("Manim") 默认 font_size=48,量出来宽 1.92、高 0.46 个单位,480p 下是 112 × 26 像素,1080p 下是 258 × 62 像素,占画面的比例不变。
线宽同样按单位算。Manim 的 Cairo 渲染器把 stroke_width 乘 0.01 当成单位长度,stroke_width=4 就是 0.04 个单位。我们量了一条水平线:480p 下约 2 像素粗,1080p 约 6 像素,4K 约 10 像素。所以在 480p 预览里觉得线有点细是正常的,导出 1080p 就和设计时想的一样了。
图片是唯一按像素数来定初始大小的。ImageMobject 默认把图片的像素高度按 1080 折算成单位:一张 540 × 270 的图进场后是 4 × 2 个单位,1080p 下正好一像素对一像素,480p 下缩成 240 × 120。
想改画布大小,改 frame_width
要让同一个 1 × 1 的方块在画面上变小,就得让画布装下更多的单位。下面把画布改成 16 × 9,宽和高一起写,写在文件顶层:
from manim import *
# 把画布改成 16 × 9 个单位:宽和高一起写,写在文件顶层
config.frame_width = 16
config.frame_height = 9
class BiggerCanvas(Scene):
def construct(self):
# 范围要自己写:NumberPlane() 的默认范围是 import 那一刻按 14.22 × 8 定死的
self.add(NumberPlane(x_range=[-8, 8, 1], y_range=[-4.5, 4.5, 1]))
unit = Square(side_length=1, color=YELLOW).set_fill(YELLOW, 0.6)
unit.move_to([0.5, 0.5, 0])
title = Text("16 × 9 的画布", font_size=40).to_edge(UP)
title.add_background_rectangle(opacity=0.85, buff=0.1)
px = config.pixel_height / config.frame_height
note = Text(f"同样 1 个单位,现在是 {px:.4g} 像素", font_size=28).to_edge(DOWN)
note.add_background_rectangle(opacity=0.85, buff=0.1)
self.play(FadeIn(unit), Write(title))
self.play(Write(note))
self.wait()
1080p 下 1 个单位变成 1080 ÷ 9 = 120 像素,横着正好 16 格。16 × 9 配 1600 × 900 的话就是每单位 100 像素,算坐标的时候很省事,我们的教程就是这么配的。
代码里有一处要留意:NumberPlane()、Axes() 这些坐标系的默认范围,是在 from manim import * 那一刻按 14.22 × 8 定下来的,之后改 config 不会跟着变。不写 x_range、y_range 的话,网格只铺中间 14 × 8 那一块,四周是空的,我们试过。
在工作区里跑这段,画面上方会出一条黄色提示,说画幅来自代码里的 frame_width、frame_height,面板上的设置被覆盖了。工作区「运行」按钮右边那个写着 480p15 的按钮是渲染设置,里面可以直接选 16:9、9:16 这些画幅和 480p 到 1080p 的画质,选了之后 frame_height 保持 8,frame_width 跟着比例算好;只有想要一块和面板不同的画布时,才需要在代码里写 config。
为什么只改 frame_height 没用
Manim 0.21.0 的相机在创建时会调一次 resize_frame_shape(),规则是:frame_width 不动,frame_height 按像素的宽高比重新算。源码里就这几行:
aspect_ratio = pixel_width / pixel_height
if fixed_dimension == 0: # 默认走这一支
frame_height = frame_width / aspect_ratio
所以真正决定「画布装多少单位」的是 frame_width 和像素比例,frame_height 只是被算出来的那个。

常见写错的地方
只写了 frame_height
config.frame_height = 16 # 想让画布高 16 个单位
1920 × 1080 下这样写,画面没有任何变化:相机按 frame_width = 14.22 和 16:9 的像素比例,把高度又算回了 8,我们数的 1 × 1 方块还是 134 像素。更麻烦的是 config.frame_height 本身确实变成了 16,to_edge、to_corner 这些按 config 找边的方法就会算错。我们让一个方块 to_edge(UP),它被摆到了 y = 7,而画面的上沿在 y = 4,方块整个出了画面。
改法是两个一起写,比例和像素一致:
config.frame_width = 16 * 16 / 9 # 28.44
config.frame_height = 16
只写 frame_width = 28.44 时,相机里的画布确实是 28.44 × 16,方块缩到 66 像素左右(理论值 1080 ÷ 16 = 67.5)。可 config.frame_height 还停在 8,to_edge(UP) 只会让东西的上沿到 y = 3.5,而画面的上沿在 y = 8。所以还是两个都写。
只改了像素,想做竖屏
config.pixel_width = 1080
config.pixel_height = 1920
命令行写 -r 1080,1920 是同一个效果,只改像素。画面不会被拉伸,相机还是守着 frame_width = 14.22,把高度算成 14.22 × 1920 ÷ 1080 = 25.28。结果画布变成了一个 14.22 × 25.28 的竖条:原来按横屏摆的东西挤在中间一小段,一个 2 × 2 的方块只有 152 像素宽;to_edge(UP) 还按 config.frame_height = 8 找边,一个 1 × 1 的方块被摆在 y = 3,离画面上沿(y = 12.64)很远。
竖屏要连画布一起改,比如 frame_width = 4.5、frame_height = 8,或者教程里用的 frame_width = 9、frame_height = 16 配 1080 × 1920。
在 construct 里改 config
class Demo(Scene):
def construct(self):
config.frame_width = 28.44 # 太晚了
config.frame_height = 16
相机在场景创建的时候就按当时的 config 建好了,construct 里再改,这一个场景用不上。我们打印了相机的画布尺寸,还是 14.22 × 7.99(-ql)。画幅相关的五个值,pixel_width、pixel_height、frame_width、frame_height、frame_rate,都写在文件顶层。
用像素去写坐标
dot.move_to([960, 540, 0]) # 想放到 1920×1080 的正中间
Manim 的坐标是单位,原点在中心,[960, 540, 0] 在画面右上方几十个画布之外。画面中心就是 ORIGIN,右上角是 config.right_side + config.top。手上只有像素坐标(左上角为 0、y 向下)的话,1080p 下这样换:x = px / 135 - 7.11,y = 4 - py / 135。
常见追问(FAQ)
Q:怎么在代码里拿到画面的边界?
A:config.frame_x_radius 和 config.frame_y_radius 是半宽、半高,默认 7.11 和 4。config.left_side、config.right_side、config.top、config.bottom 直接给的是四条边中点的坐标,可以拿来做加减。要贴边放东西,用 mob.to_edge(UP)、mob.to_corner(UR),默认留 0.5 个单位的边距。
Q:frame_width 和 pixel_width 写成不成比例的会怎样?
A:Cairo 渲染器下不会变形。相机按 frame_width 和像素宽高比重算高度,圆还是圆。我们试过 1000 × 1000 像素、画布不动,画布变成了 14.22 × 14.22,半径 1 的圆渲出来 140 × 140 像素。代价是 frame_height 和你以为的不一样了,按 config 找边的方法会偏。
Q:用 4K 渲染,画面会多出东西吗? A:不会。4K 只是每单位 270 像素,画布还是 14.22 × 8,看到的范围和 1080p 一模一样,只是更清晰,渲染也更慢。我们实测 1 × 1 方块在 4K 下是 270 × 270 像素。
Q:MovingCameraScene 放大镜头,算不算改画布?
A:效果像,但改的是相机框 self.camera.frame 的大小,config 里的画布不动,用法在运镜那一课里讲。
Q:0.22.0 在这件事上有变化吗? A:没有。本文所有数字我们在 0.21.0 和 0.22.0 上都跑了一遍,输出完全相同。工作区目前装的是 0.21.0。
画布和单位、单位和像素,在教程《画布与坐标系》的前两节里有视频讲解,旁边的代码可以边看边改,第三节接着讲坐标系和 UP、LEFT 这些方向常量。想直接动手,打开工作区。
来源:极坐标⋅XYZ · jizuobiao.xyz · 更新于 2026-10-14 · 基于 Manim Community 0.21.0,0.22.0 复核