肝了一上午, 终于写完了...
上一篇讲解了Mobject, 表示数学图形, 这一篇来讲讲Animation, 它表示动画, 是让图形动起来的关键. 另外, 文末分享清华大学DeepSeek资料.
简单的说, 动画就是两个Mobject之间插值的过程, 通过插入过渡效果的帧让动画变得平滑.
举🌰说明:
- FadeIn: Mobject从完全透明到完全不透明, 中间插值逐渐改变透明度.
- Rotate: 在初始状态和最终状态插值逐渐改变旋转角度.
.animate写法
只要一个Mobject的方法可以改变它的属性, 就可以通过.animate的写法将其变成动画.
示例代码:
from manim import *
class AnimateExample(Scene):
def construct(self):
square = Square().set_fill(WHITE, opacity=1.0)
self.add(square)
self.wait(1)
# 在一秒内逐渐填充颜色
self.play(square.animate.set_fill(RED), run_time=1)
self.wait(1)
# 在一秒内逐渐向上移动, 同时旋转60度
self.play(square.animate.shift(UP).rotate(PI / 3), run_time=1)
self.wait(1)
动画:
默认情况下play()方法渲染的动画会持续1秒钟, 使用run_time参数来改变持续时间.
自定义动画
尽管Manim有许多内置动画, 但通常这些动画都比较简单, 有时无法满足我们的需求. 这时就需要自定义动画.
要自定义动画, 就需要继承Animation类并重写interpolate_mobject()方法, 该方法有一个alpha参数, 这是控制动画的关键, 动画开始时alpha为0, 动画结束时alpha为1, 在这个过程中alpha逐渐增加. 我们要做的就是在interpolate_mobject()方法中根据alpha控制Mobject的状态.
来看一个示例, 体会一下这个alpha到底是什么意思.
示例: 倒计时5秒钟
from manim import *
class CountDown(Animation):
# 第一个参数就是要操作的Mobject, 类型是DecimalNumber
# start和end分别表示开始时间和结束时间
def __init__(self, number: DecimalNumber, start: float, end: float, **kwargs) -> None:
# 向父类传递要操作的Mobject对象
super().__init__(number, **kwargs)
self.start = start
self.end = end
def interpolate_mobject(self, alpha: float) -> None:
# 根据alpha设置要显示的值
value = (self.end - self.start) * alpha + self.start
self.mobject.set_value(value)
class CountingScene(Scene):
def construct(self):
# 创建一个DecimalNumber对象, 白色, 5倍大小
number = DecimalNumber(5).set_color(WHITE).scale(5)
self.add(number)
self.wait()
# rate_func=linear表示动画的速度是线性的, 即数字会均匀地从5减少到0
self.play(CountDown(number, 5, 0), run_time=5, rate_func=linear)
self.wait()
interpolate_mobject()方法中做到就是根据alpha值得到DecimalNumber对象要显示的值.
已知alpha=0时value=start, alpha=1时value=end, 将value看成是alpha的函数: value=k * alpha + b, 带入条件求出k和b, 就得出了15行的表达式.
在调用play()方法渲染动画时, 传入参数run_time=5, rate_func=linear, 表示在5秒内均匀地将数字从5减小到0.
动画:
如果报错, 排查一下自己有没有安装LaTex.
下载链接: Getting MiKTeX
示例: 沿着弧线交换位置
from manim import *
import numpy as np
class SwapAlongCircle(Animation):
def __init__(self, mobject1, mobject2, **kwargs):
self.mobject1 = mobject1
self.mobject2 = mobject2
# 计算圆心和半径, get_center()返回类型是ndarray, [x,y,z]表示三维空间的坐标
self.center = (mobject1.get_center() + mobject2.get_center()) / 2
self.radius = np.linalg.norm(mobject1.get_center() - mobject2.get_center()) / 2
# 计算初始角度
delta = mobject1.get_center() - mobject2.get_center()
self.initial_angle = np.arctan2(delta[1], delta[0]) # 初始角度
# 随便传一个Mobject给父类
super().__init__(mobject1, **kwargs)
def interpolate_mobject(self, alpha: float) -> None:
# 计算当前角度
angle = self.initial_angle + alpha * PI
# 计算两个Mobject的当前位置, 两个Mobject都沿着逆时针方向移动
pos1 = self.center + self.radius * np.array([np.cos(angle), np.sin(angle), 0])
pos2 = self.center - self.radius * np.array([np.cos(angle), np.sin(angle), 0])
# 更新两个Mobject的位置
self.mobject1.move_to(pos1)
self.mobject2.move_to(pos2)
class SwapAlongCircleScene(Scene):
def construct(self):
square1 = Square().set_fill(BLUE, opacity=1).shift(UP * 2 + LEFT)
square2 = Square().set_fill(RED, opacity=1).shift(DOWN * 2 + RIGHT)
self.add(square1, square2)
self.wait(1)
self.play(SwapAlongCircle(square1, square2), run_time=3, rate_functions=linear)
self.wait(1)
这里主要是计算角度随alpha的变化, 然后配合三角函数算出两个Mobject的位置. (还需要点数学功底👻!)
另外, ndarray支持高效的数值计算, 可以避免循环. 合理的运用可以简化代码.
动画:
Mobject中的坐标
之前的示例只用过Mobject的get_center()方法, 它表示Mobject的中心, 除此之外, 还有许多其他方法用于返回Mobject中特定点的坐标. 这里一并列举:
- get_center(): 中心点
- get_top(): 上方的点
- get_bottom: 下方的点
- get_start(): 边界的起点
- get_end(): 边界的终点
- point_from_proportion(): 从边界的起点到终点的路径上指定比例的点
示例代码:
from manim import *
class MobjectExample(Scene):
def construct(self):
p1 = [-2, -1, 0]
p2 = [-1, -1, 0]
p3 = [1, 1, 0]
p4 = [2, 1, 0]
a = Line(p1, p2).append_points(Line(p2, p3).points).append_points(Line(p3, p4).points)
point_start = a.get_start()
point_end = a.get_end()
point_center = a.get_center()
# to_edge(UR)表示将对象的边缘对齐到屏幕右上角, 距离为默认的0.5
self.add(Text(f"a.get_start() = {np.round(point_start, 2).tolist()}", font_size=24).to_edge(UR).set_color(YELLOW))
# self.mobjects[-1]表示Scene中添加的最后一个对象
self.add(Text(f"a.get_end() = {np.round(point_end, 2).tolist()}", font_size=24).next_to(self.mobjects[-1], DOWN).set_color(RED))
self.add(Text(f"a.get_center() = {np.round(point_center, 2).tolist()}", font_size=24).next_to(self.mobjects[-1], DOWN).set_color(BLUE))
self.add(Dot(a.get_start()).set_color(YELLOW).scale(2))
self.add(Dot(a.get_end()).set_color(RED).scale(2))
self.add(Dot(a.get_top()).set_color(GREEN_A).scale(2))
self.add(Dot(a.get_bottom()).set_color(GREEN_D).scale(2))
self.add(Dot(a.get_center()).set_color(BLUE).scale(2))
# a.point_from_proportion(0.5)表示路径上的中点
self.add(Dot(a.point_from_proportion(0.5)).set_color(ORANGE).scale(2))
# 使用Python的列表解析并解包
self.add(*[Dot(x) for x in a.points])
self.add(a)
to_edge(UR)表示将Mobject边缘对齐到屏幕右上角, 距离为默认的0.5. 如果添加参数buff=0则表示Mobject边缘贴紧屏幕右上角.
self.add(*[Dot(x) for x in a.points]) 这一行表示使用a中的点创建Dot对象, 使用*将列表解包成一个个单独的对象.
动画:
奇怪的动画
Mobject可以看作是一个个点组成的图形, 所以在Transorm转换的时候Manim就会将一个个点转换到对应的位置. 正是由于这种机制, 导致可能出现奇怪的动画.
示例代码:
from manim import *
class MobjectExample(Scene):
def construct(self):
square1 = Square()
square2 = Square().shift(RIGHT).rotate(PI)
self.play(Transform(square1, square2), run_time=2)
动画:
解决方法: 使用numpy的roll()方法将Mobject中的点重新排列.
from manim import *
class MobjectExample(Scene):
def construct(self):
square1 = Square()
# 逆时针旋转180度
square2 = Square().shift(RIGHT).rotate(PI)
points = square1.points
print(points)
# 顺时针旋转180度
square2.points = np.roll(points, int(len(points) / 2), axis=0)
self.play(Transform(square1, square2), run_time=2)
动画:
同样的道理, 可以让下面示例中变换具有对称效果.
from manim import *
class ExampleRotation(Scene):
def construct(self):
square1 = Square().set_color(RED).shift(LEFT)
circle1 = Circle().set_color(RED).shift(LEFT)
square2 = Square().set_color(BLUE).shift(RIGHT)
circle2 = Circle().set_color(BLUE).shift(RIGHT)
points = square2.points
# 顺时针旋转90度
points = np.roll(points, int(len(points) / 4), axis=0)
square2.points = points
self.play(Transform(square1, circle1), Transform(square2, circle2), run_time=1)
动画:
以上就是本篇的全部内容了!
最后再分享一份清华大学DeepSeek的资料, 公众号:算法铁金库, 输入暗号: 106. 有需要的同学自取!