Python 做数学动画,别一上来就写公式,先把时间轴拎明白
很多人第一次用 Python 做数学动画,手一抖就开始堆公式,Tex()、MathTex()、渐变、旋转、镜头推进,一口气全上。结果做出来的东西很像 PPT 加特效,不像“动画”。
这事我一开始也走过弯路。后来我发现问题根本不在公式,也不在库,主要是时间轴没想清楚:什么东西先出现,谁跟着谁变化,哪个量是“算出来的”,哪个量只是“画出来的”。这几个点一乱,画面再花也白搭。
拿 Python 这条路来说,真想把数学动画做得有点质感,我反而建议先把脑子从“画图”切到“驱动对象变化”。这个感觉一旦对了,味道马上就不一样了。整体气质上,我参考了几篇偏实战、偏现场感的技术文写法,但这里不照着它们的结构走,只借它们那种“真做过”的劲儿。
我平时更愿意用 manim 这类路线。原因很简单,不是因为它最“高级”,而是它把动画这件事拆得比较像代码:对象、坐标、变换、时间。程序员看这个不费劲。
先看一段最小可用的东西,别急着整复杂。
from manim import *
classSinWaveDemo(Scene):
defconstruct(self):
axes = Axes(
x_range=[-4, 4, 1],
y_range=[-2, 2, 1],
x_length=8,
y_length=4,
axis_config={"include_numbers": False},
)
graph = axes.plot(lambda x: np.sin(x), x_range=[-4, 4])
title = Text("y = sin(x)").scale(0.6).to_edge(UP)
self.play(Create(axes))
self.play(Write(title))
self.play(Create(graph), run_time=2)
self.wait(1)
这段代码没什么花活,但它已经有“动画语义”了:先建坐标轴,再写标题,再把曲线画出来。顺序很朴素,观众也看得懂。
很多人写到这里就开始往里塞知识点了,什么泰勒展开、导数、切线、积分面积,一股脑往一个场景里灌。我一般不太信这种写法。数学动画最怕的不是内容少,是画面里同时发生的事太多。观众眼睛在追动画,脑子在跟逻辑,你还让他同时记公式变形,基本等于逼人分神。
更稳一点的写法,是只做一件事,然后把这件事做透。
比如你要讲“导数是变化率”,别先甩定义,先把“点在动,斜率也在动”这件事画出来。这个画面一出来,后面的解释就顺了。
from manim import *
classTangentLineDemo(Scene):
defconstruct(self):
axes = Axes(
x_range=[-3, 3, 1],
y_range=[-1, 9, 1],
x_length=7,
y_length=5,
)
curve = axes.plot(lambda x: x**2, x_range=[-2.5, 2.5])
tracker = ValueTracker(-2)
dot = always_redraw(
lambda: Dot(
axes.c2p(tracker.get_value(), tracker.get_value() ** 2)
)
)
tangent = always_redraw(
lambda: axes.get_secant_slope_group(
x=tracker.get_value(),
graph=curve,
dx=0.01,
secant_line_length=2,
)
)
label = always_redraw(
lambda: MathTex(
f"x={tracker.get_value():.2f}"
).scale(0.6).to_corner(UR)
)
self.play(Create(axes), Create(curve))
self.play(FadeIn(dot), FadeIn(tangent), FadeIn(label))
self.play(tracker.animate.set_value(2), run_time=4, rate_func=linear)
self.wait()
这段代码里真正有意思的不是 x**2,而是 ValueTracker 和 always_redraw。
我第一次真把数学动画做顺,就是在这两个东西上开窍的。ValueTracker 负责“时间里谁在变”,always_redraw 负责“变了以后怎么重画”。你把这两个家伙用顺,很多看着很玄的效果,其实就是老老实实重算、重绘。
说白了——这个词我平时不太爱用,但这里确实挺合适——数学动画的底层味道,不是美术,而是状态驱动。
再往前走一步,逼格通常不是靠炫,而是靠节奏。比如同样是讲傅里叶级数,有人一上来堆十几项展开式,屏幕上满天飞公式;有的人只做两件事:左边原函数,右边逼近曲线,随着项数增加一点点贴近。后者往往更稳。
代码大概可以写成这样:
from manim import *
classFourierFeeling(Scene):
defconstruct(self):
axes = Axes(
x_range=[-PI, PI, PI/2],
y_range=[-2, 2, 1],
x_length=8,
y_length=4,
)
n_tracker = ValueTracker(1)
defapprox(x, n):
s = 0
for k in range(1, int(n) + 1):
s += np.sin((2 * k - 1) * x) / (2 * k - 1)
return4 / PI * s
graph = always_redraw(
lambda: axes.plot(
lambda x: approx(x, n_tracker.get_value()),
x_range=[-PI, PI]
)
)
text = always_redraw(
lambda: MathTex(
f"n={int(n_tracker.get_value())}"
).scale(0.7).to_edge(UP)
)
self.play(Create(axes), Create(graph), Write(text))
self.play(n_tracker.animate.set_value(15), run_time=6, rate_func=linear)
self.wait()
你看,这里也没搞什么离谱操作,甚至连配色都没折腾。但观感通常不会差,因为“项数增长”这个核心变量被抓住了。
最后放一个我自己比较认可的选题思路,适合写成第一支像样的作品:
# 主题:单位圆 -> 正弦函数
# 场景1:画圆,点开始转
# 场景2:把点的纵坐标投影到右侧
# 场景3:投影轨迹逐步形成 sin 曲线
# 场景4:补一个角度和高度的数值标签
这种题目好就好在,数学关系天然带动态,画面也不会干。你不需要硬造戏,图形自己就有故事。
Python 做数学动画这事,门槛其实没想象中那么高。难的不是库,也不是公式,而是你能不能像写程序一样去拆动画:谁是状态,谁是结果,谁应该先出现,谁应该慢一点。