使用Manim来绘制正弦函数和余弦函数的图像

今天从互联网的微信公众号“数与理”上找到一篇非常不错的文章,绘制正弦函数和余弦函数的图像,代码做了测试,非常的不错,先来看正弦函数的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
from manim import *
import numpy as np

class v2_1(Scene):
def construct(self):
self.show_axis() # 显示坐标轴
self.show_circle() # 显示单位圆
self.move_dot_and_draw_curve() # 移动点并绘制正弦曲线
self.wait(2)

def show_axis(self):
"""建立坐标轴"""

self.ratio = 1.2
self.axes = Axes(x_range=[0, 14, PI],
y_range=[-3, 3],
x_length=14 / self.ratio,
y_length=6 / self.ratio,
axis_config={"color": BLUE, "stroke_width": 3},
y_axis_config={'tick_size': 0},
tips=True
).shift(RIGHT)
labels = self.axes.get_axis_labels(
Tex("x").scale(0.7), Text("y").scale(0.45))
self.add(self.axes, labels)

# 添加 x 轴标签
x_labels = [MathTex(r"\pi").scale(0.8),
MathTex(r"2\pi").scale(0.8),
MathTex(r"3\pi").scale(0.8),
MathTex(r"4\pi").scale(0.8)]
for i, label in enumerate(x_labels):
# self.axes.c2p(x, y):将坐标轴的点 (x, y) 映射为画布上的点
label.move_to(self.axes.c2p((i+1) * PI, -0.3))
self.add(label)

# 正弦线的起点,为坐标轴的原点
self.curve_start = self.axes.get_origin()

# 将 x 轴向左延长,放置单位圆
x_start = self.axes.c2p(-2.5, 0)
x_axis = Line(x_start, self.curve_start,
color=BLUE,
stroke_width=3)
self.add(x_axis)

# 为单位圆建立一条虚线 y 轴坐标轴
y_start, y_end = self.axes.c2p(-1, -2), self.axes.c2p(-1, 2)
y_axis = DashedLine(y_start, y_end, color=GRAY)
self.add(y_axis)

# 坐标轴的单位长度
self.unit_size = np.round(self.axes.x_axis.get_unit_size(), 2)

def show_circle(self):
"""显示单位圆"""

# 单位圆的圆心为(-1, 0)
self.origin_point = self.axes.c2p(-1, 0)
# set_z_index(10) 保持圆心在较上的图层
origin_dot = Dot(self.origin_point, color=RED, radius=0.06
).set_z_index(10)

# 单位圆
self.circle = Circle(radius=self.unit_size,
color=RED,
stroke_width=3)
self.circle.move_to(self.origin_point)
self.add(origin_dot, self.circle)

def move_dot_and_draw_curve(self):
"""移动圆上的点并绘制正弦曲线"""

# 添加时间跟踪器,用于动画
time_tracker = ValueTracker(0)

def update_dot_position(mob):
'''圆上动点的位置更新器'''

# TAU 为 Manim 常量 2π,
# time_tracker 从 0 变为 1,动点在圆上旋转 2 周,
# angle 则为总的旋转角度(弧度制)
angle = time_tracker.get_value() * TAU * 2
mob.move_to(self.circle.point_at_angle(angle))

# 圆上的动点
dot = Dot(self.circle.point_at_angle(0),
radius=0.08,
color=YELLOW,
z_index=10)

# dot 会根据 time_tracker 值的改变而改变位置
dot.add_updater(update_dot_position)
self.add(dot)

# 创建圆半径线
# always_redraw:在动画的每一帧中重绘图形
radius_line = always_redraw(lambda:
Line(self.origin_point,
dot.get_center(),
color=BLUE,
stroke_width=2))
self.add(radius_line)

def update_curve_start_dot_position(mob):
'''正弦线上动点的位置更新器'''

# 正弦线上动点的 x 坐标为起点 + 圆动点转过的弧度 * 坐标轴比例,
# y 坐标为圆动点的 y 坐标

position = [self.curve_start[0] + time_tracker.get_value() *
TAU * 2 * self.unit_size,
dot.get_center()[1], 0]
mob.move_to(position)

# 正弦线上的动点
curve_start_dot = Dot(self.curve_start, color=BLUE, radius=0.08
).set_z_index(10)
curve_start_dot.add_updater(update_curve_start_dot_position)
self.add(curve_start_dot)

# 创建连接线(圆上点到曲线)
connection_line = always_redraw(lambda:
DashedLine(dot.get_center(), curve_start_dot.get_center(),
color=YELLOW_A, stroke_width=2, dash_length=0.1))
self.add(connection_line)

# 初始化正弦曲线
self.sine_curve = Line(self.curve_start, self.curve_start,
color=YELLOW_D,stroke_width=3)

def update_sine_curve(mob):
'''更新正弦曲线,正弦线由小段的首尾相连的直线组成'''
new_point = curve_start_dot.get_center()
new_line = Line(self.sine_curve.get_end(), new_point)
self.sine_curve.append_points(new_line.points)

self.sine_curve.add_updater(update_sine_curve)

self.add(self.sine_curve)

# 创建正弦曲线方程显示
equation = MathTex(r"y = \sin(x)", font_size=36, color=RED)
equation.to_corner(UR, buff=0.5)
self.add(equation)

# 开始动画
self.play(time_tracker.animate.set_value(1),
rate_func=linear,
run_time=9)

# 移除更新器
dot.remove_updater(update_dot_position)

我个人并不是非常理解这段代码,只是在这里做一个记录,下面是代码的视频效果

再来看余弦函数的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
from manim import *
import numpy as np

class v2_2(Scene):
'''修改move_dot_and_draw_curve()方法,绘制余弦函数'''

def construct(self):
self.show_axis()
self.show_circle()
self.move_dot_and_draw_curve() # 移动点并绘制余弦曲线
self.wait(2)

def show_axis(self):
self.ratio = 1.2
self.axes = Axes(x_range=[0, 14, PI],
y_range=[-3, 3],
x_length=14 / self.ratio,
y_length=6 / self.ratio,
axis_config={"color": BLUE, "stroke_width": 3},
y_axis_config={'tick_size': 0},
tips=True).shift(RIGHT)
labels = self.axes.get_axis_labels(
Tex("x").scale(0.7), Text("y").scale(0.45))
self.add(self.axes, labels)

x_labels = [MathTex(r"\pi").scale(0.8),
MathTex(r"2\pi").scale(0.8),
MathTex(r"3\pi").scale(0.8),
MathTex(r"4\pi").scale(0.8)]
for i, label in enumerate(x_labels):
label.move_to(self.axes.c2p((i+1) * PI, -0.3))
self.add(label)

self.curve_start = self.axes.get_origin()

x_start = self.axes.c2p(-2.5, 0)
x_axis = Line(x_start, self.curve_start,
color=BLUE, stroke_width=3)
self.add(x_axis)

y_start, y_end = self.axes.c2p(-1, -2), self.axes.c2p(-1, 2)
y_axis = DashedLine(y_start, y_end, color=GRAY)
self.add(y_axis)

self.unit_size = np.round(self.axes.x_axis.get_unit_size(), 2)

def show_circle(self):
self.origin_point = self.axes.c2p(-1, 0)

origin_dot = Dot(self.origin_point, color=RED, radius=0.06
).set_z_index(10)

self.circle = Circle(radius=self.unit_size,
color=RED, stroke_width=3)
self.circle.move_to(self.origin_point)

self.add(origin_dot, self.circle)

def move_dot_and_draw_curve(self):
"""移动圆上的点并绘制余弦曲线"""

time_tracker = ValueTracker(0)

def update_dot_position(mob):
angle = time_tracker.get_value() * TAU * 2
mob.move_to(self.circle.point_at_angle(angle))

dot = Dot(self.circle.point_at_angle(0),
radius=0.08,
color=YELLOW,
z_index=10
)
dot.add_updater(update_dot_position)
self.add(dot)

radius_line = always_redraw(lambda:
Line(self.origin_point, dot.get_center(),
color=BLUE, stroke_width=2))
self.add(radius_line)

def update_curve_start_dot_position(mob):
'''
余弦线上动点的位置更新器:
x 坐标为起点 + 圆动点转过的弧度 * 坐标轴比例;
y 坐标为圆动点的 x 坐标(相对于圆心的坐标)
'''
# 计算圆上点相对于圆心的x坐标(即cos值)
relative_x = dot.get_center()[0] - self.origin_point[0]

# x坐标:从原点开始,根据转过的角度移动
x_pos = self.curve_start[0] + time_tracker.get_value() * TAU * 2 * self.unit_size

# y坐标:使用圆上点的x坐标(即余弦值)
# 注意:圆上的点坐标:x = cos(θ), y = sin(θ)
# 相对于圆心,所以点的x坐标就是cos(θ)
y_pos = relative_x

position = [x_pos, y_pos, 0]
mob.move_to(position)

# 余弦线上的动点起始位置为(0, 1)
# 当角度为0时,cos(0)=1,所以起点在(0, 1)
curve_start_dot = Dot(self.axes.c2p(0, 1), color=BLUE, radius=0.08
).set_z_index(10)
curve_start_dot.add_updater(update_curve_start_dot_position)
self.add(curve_start_dot)

connection_line = always_redraw(lambda:
DashedLine(dot.get_center(), curve_start_dot.get_center(),
color=YELLOW_A, stroke_width=2, dash_length=0.1))
self.add(connection_line)

# 创建余弦曲线
self.cosine_curve = Line(curve_start_dot.get_center(),
curve_start_dot.get_center(),
color=YELLOW_D, stroke_width=3)

def update_cosine_curve(mob):
'''更新余弦曲线,添加新的点'''
new_point = curve_start_dot.get_center()
# 只有当新点与最后一个点不同时才添加
if len(self.cosine_curve.points) == 0 or np.linalg.norm(new_point - self.cosine_curve.get_end()) > 0.01:
new_line = Line(self.cosine_curve.get_end(), new_point)
self.cosine_curve.append_points(new_line.points)

self.cosine_curve.add_updater(update_cosine_curve)
self.add(self.cosine_curve)

# 展示余弦函数公式
equation = MathTex(r"y = \cos(x)", font_size=36, color=RED)
equation.to_corner(UR, buff=0.5)
self.add(equation)

# 运行动画,让点移动并绘制余弦曲线
self.play(time_tracker.animate.set_value(1),
rate_func=linear,
run_time=9)

dot.remove_updater(update_dot_position)
curve_start_dot.remove_updater(update_curve_start_dot_position)
self.cosine_curve.remove_updater(update_cosine_curve)

上面的代码,我借助 deepseek 做了适当的调整,下面是视频文件演示

回到文章的起点,主要是为了做一个记录,代码效果还是非常不错的,如果要是制作课件的话,非常值得借鉴。