1 # -*- coding: utf-8 -*-
2 from manim import *
3 import sys
4 import io
5
6 # 设置标准输出编码
7 sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
8
9 class ZigzagGraph(Scene):
10 def construct(self):
11 # === 1. 节点位置定义 ===
12 positions = {
13 -1: [-6, 2, 0],
14 2: [-6, 0, 0],
15 4: [-4, 2, 0],
16 -3: [-4, 0, 0],
17 -5: [-2, 2, 0],
18 6: [-2, 0, 0],
19 8: [0, 2, 0],
20 -7: [0, 0, 0],
21 -9: [2, 2, 0],
22 10: [2, 0, 0],
23 'A': [4, 2, 0],
24 'B': [6, 2, 0],
25 'C': [6, 0, 0],
26 'D': [8, 0, 0],
27 }
28
29 # === 2. 创建节点 ===
30 nodes = {}
31 for label, pos in positions.items():
32 if isinstance(label, int):
33 color = RED if label < 0 else GREEN
34 # 使用LaTeX的\textbf{}来加粗
35 node = MathTex(r"\textbf{" + str(label) + "}", color=color, font_size=36)
36 else:
37 node = MathTex(r"\textbf{" + label + "}", color=BLUE, font_size=36)
38 node.move_to(pos)
39 nodes[label] = node
40
41 # 创建省略号
42 dots1 = MathTex(r"\cdots", font_size=40).move_to([positions['A'][0], positions[10][1], 0])
43 dots2 = MathTex(r"\cdots", font_size=40).move_to([positions['D'][0], positions['B'][1], 0])
44
45 # === 3. 创建箭头 ===
46 arrows = []
47 connections = [
48 (-1, 2), (2, -3), (-3, 4), (4, -5), (-5, 6),
49 (6, -7), (-7, 8), (8, -9), (-9, 10),
50 (10, dots1), (dots1, 'A'), ('A', 'B'),
51 ('B', 'C'), ('C', 'D'), ('D', dots2)
52 ]
53
54 for start, end in connections:
55 start_obj = nodes.get(start) or (dots1 if start is dots1 else dots2 if start is dots2 else None)
56 end_obj = nodes.get(end) or (dots1 if end is dots1 else dots2 if end is dots2 else None)
57 if start_obj and end_obj:
58 arrow = Arrow(start_obj.get_center(), end_obj.get_center(), buff=0.3, stroke_width=3, color=GRAY)
59 arrows.append(arrow)
60
61 # === 4. 创建高亮圆圈 ===
62 # 黄色圆圈(负奇数)
63 yellow_labels = [-1, -3, -5, -7, -9]
64 yellow_circles = [Circle(radius=0.5, color=YELLOW, stroke_width=5, fill_opacity=0.2).move_to(nodes[label].get_center())
65 for label in yellow_labels]
66
67 # 蓝色圆圈(dots1, B, D)
68 blue_targets = [dots1, nodes['B'], nodes['D']]
69 blue_circles = [Circle(radius=0.5, color=BLUE, stroke_width=5, fill_opacity=0.2).move_to(obj.get_center())
70 for obj in blue_targets]
71
72 # === 5. 整体布局调整 ===
73 full_group = VGroup(
74 *nodes.values(), dots1, dots2, *arrows, *yellow_circles, *blue_circles
75 )
76 full_group.scale(0.6).shift(LEFT * 1 + UP * 2)
77
78 # 分组管理
79 static_elements = VGroup(*nodes.values(), dots1, dots2)
80 arrow_elements = VGroup(*arrows)
81 yellow_circle_elements = VGroup(*yellow_circles)
82 blue_circle_elements = VGroup(*blue_circles)
83
84 # === 6. 动画流程 ===
85 # 添加静态元素
86 self.add(static_elements)
87
88 # 动画1:逐步绘制路径(使用基础的rate_func)
89 self.play(LaggedStart(*[Create(arrow) for arrow in arrow_elements], lag_ratio=0.1),
90 run_time=4, rate_func=smooth)
91
92 self.wait(1.5)
93
94 # === 新增:提前显示问题部分 ===
95 # 创建问题容器
96 questions_container = VGroup()
97 questions_container.next_to(full_group, DOWN, buff=1.5).shift(LEFT * 3)
98
99 # 问题标题
100 questions_title = Text("思考问题", font_size=32, color=YELLOW)
101
102 # 问题内容
103 questions = [
104 "(1) A位置的数是正数还是负数?",
105 "(2) A,B,C,D中哪些位置是负数?",
106 "(3) 第2046个数是正数还是负数?在哪个位置?"
107 ]
108
109 question_elements = [questions_title]
110 for q in questions:
111 question = Text(q, font_size=24, color=WHITE)
112 question_elements.append(question)
113
114 # 排列问题元素
115 questions_container.add(*question_elements)
116 questions_container.arrange(DOWN, aligned_edge=LEFT, buff=0.5)
117
118 # 逐个显示问题
119 for element in question_elements:
120 self.play(Write(element, run_time=0.8), element.animate.scale(1.05).scale(1), run_time=0.8)
121 self.wait(0.8)
122
123 self.wait(2)
124
125 # 问题淡出
126 self.play(FadeOut(questions_container, shift=DOWN*0.3, scale=0.9), run_time=1)
127 self.wait(0.5)
128
129 # === 7. 步骤说明 ===
130 # 创建步骤容器
131 steps_container = VGroup()
132 steps_container.next_to(full_group, DOWN, buff=1.5).shift(LEFT * 3)
133
134 # 标题
135 title = Text("规律分析", font_size=32, color=YELLOW)
136
137 # Step 1
138 step1 = VGroup(
139 Text("步骤 1:符号规律分析", font_size=26, color=YELLOW),
140 Text("排列:-1(负)、2(正)、-3(负)、4(正)、-5(负)、6(正)……", font_size=22),
141 Text("规律:奇数为负数,偶数为正数", font_size=22, color=GREEN)
142 ).arrange(DOWN, aligned_edge=LEFT, buff=0.3)
143
144 # Step 2
145 step2 = VGroup(
146 Text("步骤 2:位置循环规律", font_size=26, color=YELLOW),
147 Text("每4个数为一个位置循环周期:上 → 下 → 右 → 上", font_size=22),
148 Text("每个周期:上(-奇)→下(+偶)→右(-奇)→上(+偶)", font_size=22, color=GREEN)
149 ).arrange(DOWN, aligned_edge=LEFT, buff=0.3)
150
151 # 排列所有元素
152 steps_container.add(title, step1, step2)
153 steps_container.arrange(DOWN*0.8, aligned_edge=LEFT, buff=0.4)
154
155 # 逐个显示元素(带动画)
156 elements_to_show = [title, step1[0], step1[1], step1[2], step2[0], step2[1], step2[2]]
157
158 for element in elements_to_show:
159 self.play(Write(element, run_time=0.8), element.animate.scale(1.05).scale(1), run_time=0.8)
160 self.wait(0.8)
161
162 self.wait(2)
163
164 # 步骤消失(带动画)
165 self.play(FadeOut(steps_container, shift=DOWN*0.3, scale=0.9), run_time=1)
166 self.wait(0.5)
167
168 # === 8. 圆圈高亮动画(移到规律分析之后)===
169 # 动画2:黄色高亮(同时出现)
170 self.play(FadeIn(yellow_circle_elements, scale=0.8),
171 run_time=1.5)
172 self.wait(0.5)
173
174 # 动画3:蓝色高亮(依次出现)
175 self.play(LaggedStart(*[FadeIn(circle, scale=0.8) for circle in blue_circles], lag_ratio=0.3),
176 run_time=2)
177
178 self.wait(1.5)
179
180 # === 9. 问答内容 ===
181 qa_container = VGroup()
182 # 设置问答内容的初始位置在图形下方更远的位置
183 qa_container.next_to(full_group, DOWN, buff=3).shift(LEFT * 3)
184
185 answers = [
186 "A在\"上\"位置,对应周期内第4个数(偶数)→ 正数",
187 "B(第5个,奇数→负)、D(第7个,奇数→负)位置是负数",
188 "2046是偶数→正数;2046÷4=511余2→对应\"下\"位置(C位置)"
189 ]
190
191 # 创建问答元素
192 qa_elements = []
193 for i, (q, a) in enumerate(zip(questions, answers)):
194 question = Text(q, font_size=24, color=YELLOW)
195 answer = Text(a, font_size=22)
196
197 # 设置初始位置(在更下方)
198 if i == 0:
199 question.move_to(qa_container)
200 else:
201 question.next_to(qa_elements[-1][1], DOWN, buff=0.6)
202
203 answer.next_to(question, DOWN, buff=0.2)
204 answer.set_color_by_gradient(GREEN, BLUE)
205
206 qa_elements.append((question, answer))
207
208 # 逐个显示问答(从下方向上移动到合适位置)
209 for question, answer in qa_elements:
210 # 保存最终位置
211 final_question_pos = question.get_center()
212 final_answer_pos = answer.get_center()
213
214 # 设置初始位置在更下方
215 question.shift(UP * 1.5)
216 answer.shift(UP * 1.5)
217
218 # 问题从下方向上移动并显示
219 self.play(
220 question.animate.move_to(final_question_pos),
221 FadeIn(question),
222 run_time=0.8
223 )
224 self.wait(2)
225
226 # 答案从下方向上移动并显示
227 self.play(
228 answer.animate.move_to(final_answer_pos),
229 FadeIn(answer),
230 run_time=0.8
231 )
232 self.wait(2.5)
233
234 self.wait(3)
235
236 # # 最终淡出效果
237 # self.play(
238 # full_group.animate.scale(0.9).fade(0.5),
239 # *[FadeOut(elem) for pair in qa_elements for elem in pair],
240 # run_time=1.5
241 # )
242 # self.wait()