|
|
# difference_of_squares_final.py
|
|
|
from manim import *
|
|
|
import numpy as np
|
|
|
|
|
|
class SquareDifference(Scene):
|
|
|
def create_bubble(self, text, direction=UP):
|
|
|
"""创建自定义对话气泡(完全兼容版)"""
|
|
|
# 基础形状组件
|
|
|
bubble = VGroup()
|
|
|
|
|
|
# 文本内容(确保中文字体可用)
|
|
|
text_mob = Text(text, font="Microsoft YaHei", font_size=24)
|
|
|
text_mob.scale(0.8)
|
|
|
|
|
|
# 背景框
|
|
|
rect = RoundedRectangle(
|
|
|
width=text_mob.width + 0.5,
|
|
|
height=text_mob.height + 0.3,
|
|
|
corner_radius=0.2,
|
|
|
fill_color=WHITE,
|
|
|
fill_opacity=1,
|
|
|
stroke_color=BLACK,
|
|
|
stroke_width=2
|
|
|
)
|
|
|
|
|
|
# 箭头方向处理
|
|
|
arrow = Triangle().scale(0.3)
|
|
|
arrow.set_fill(WHITE, 1)
|
|
|
arrow.set_stroke(BLACK, 2)
|
|
|
|
|
|
# 使用向量判断方向
|
|
|
if np.array_equal(direction, UP):
|
|
|
arrow.rotate(-PI/2)
|
|
|
bubble.add(rect, arrow)
|
|
|
bubble.arrange(DOWN, buff=0.1)
|
|
|
elif np.array_equal(direction, DOWN):
|
|
|
arrow.rotate(PI/2)
|
|
|
bubble.add(arrow, rect)
|
|
|
bubble.arrange(DOWN, buff=0.1)
|
|
|
else: # 默认上方
|
|
|
arrow.rotate(-PI/2)
|
|
|
bubble.add(rect, arrow)
|
|
|
bubble.arrange(DOWN, buff=0.1)
|
|
|
|
|
|
# 定位文本
|
|
|
text_mob.move_to(rect)
|
|
|
bubble.add(text_mob)
|
|
|
return bubble
|
|
|
|
|
|
def construct(self):
|
|
|
# 高清配置
|
|
|
config.frame_width = 16
|
|
|
config.frame_height = 9
|
|
|
config.pixel_height = 1440
|
|
|
config.pixel_width = 2560
|
|
|
|
|
|
# 参数配置
|
|
|
a = 4
|
|
|
b = 1
|
|
|
colors = {
|
|
|
"a": "#1F77B4",
|
|
|
"b": "#FF7F0E",
|
|
|
"text": "#FFFFFF",
|
|
|
"bg": "#2E2E2E"
|
|
|
}
|
|
|
self.camera.background_color = colors["bg"]
|
|
|
step_delay = 2.5
|
|
|
|
|
|
# ========== 标题部分 ==========
|
|
|
title = Text("东师理想数学课件生成助手",
|
|
|
font_size=48,
|
|
|
color=colors["text"],
|
|
|
weight=BOLD,
|
|
|
stroke_width=8,
|
|
|
stroke_color=BLACK)
|
|
|
|
|
|
sub_title = Text("平方差公式可视化演示",
|
|
|
font_size=32,
|
|
|
color=colors["text"],
|
|
|
stroke_width=4,
|
|
|
stroke_color=BLACK).next_to(title, DOWN)
|
|
|
|
|
|
self.play(
|
|
|
Write(title, run_time=1.5),
|
|
|
FadeIn(sub_title, shift=UP),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay)
|
|
|
self.play(
|
|
|
FadeOut(title, shift=UP),
|
|
|
FadeOut(sub_title, shift=DOWN),
|
|
|
run_time=1.5
|
|
|
)
|
|
|
|
|
|
# ========== 公式推导部分 ==========
|
|
|
formula_steps = VGroup(
|
|
|
MathTex("(", "a", "+", "b", ")(", "a", "-", "b", ")", font_size=40),
|
|
|
MathTex("=", "a", "^2", "-", "b", "^2", font_size=40),
|
|
|
MathTex("=", f"{a**2}", "-", f"{b**2}", font_size=40),
|
|
|
MathTex("=", f"{(a+b)*(a-b)}", font_size=48, color=GREEN)
|
|
|
).arrange(DOWN, aligned_edge=LEFT, buff=0.8)
|
|
|
|
|
|
formula_board = SurroundingRectangle(
|
|
|
formula_steps,
|
|
|
color=WHITE,
|
|
|
stroke_width=2,
|
|
|
fill_color=BLACK,
|
|
|
fill_opacity=0.7,
|
|
|
corner_radius=0.2
|
|
|
)
|
|
|
|
|
|
self.play(
|
|
|
DrawBorderThenFill(formula_board),
|
|
|
FadeIn(formula_steps, shift=UP),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay)
|
|
|
|
|
|
# ========== 坐标系部分(最终修复) ==========
|
|
|
# 使用新版背景线直接生成网格
|
|
|
axes = Axes(
|
|
|
x_range=[0, a+b+1, 1],
|
|
|
y_range=[0, a+b+1, 1],
|
|
|
x_length=8,
|
|
|
y_length=6,
|
|
|
axis_config={
|
|
|
"color": colors["text"],
|
|
|
"stroke_width": 3,
|
|
|
"include_numbers": True
|
|
|
}
|
|
|
).shift(DOWN*0.5)
|
|
|
|
|
|
|
|
|
# 大正方形动画
|
|
|
big_square = Rectangle(
|
|
|
width=a+b,
|
|
|
height=a+b,
|
|
|
color=colors["a"],
|
|
|
fill_opacity=0.4,
|
|
|
stroke_width=4
|
|
|
).move_to(axes.c2p((a+b)/2, (a+b)/2))
|
|
|
|
|
|
self.play(
|
|
|
formula_board.animate.scale(0.8).to_edge(UP, buff=0.5),
|
|
|
Create(axes, run_time=2),
|
|
|
DrawBorderThenFill(big_square),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay)
|
|
|
|
|
|
|
|
|
# ========== 剩余动画部分 ==========
|
|
|
# 垂直分割线
|
|
|
v_line = Line(
|
|
|
start=axes.c2p(a, 0),
|
|
|
end=axes.c2p(a, a+b),
|
|
|
color=colors["text"],
|
|
|
stroke_width=4
|
|
|
)
|
|
|
v_label = MathTex("a",
|
|
|
color=colors["text"],
|
|
|
stroke_width=3,
|
|
|
stroke_color=BLACK).next_to(v_line, RIGHT, buff=0.2)
|
|
|
|
|
|
# 水平分割线
|
|
|
h_line = Line(
|
|
|
start=axes.c2p(0, a),
|
|
|
end=axes.c2p(a+b, a),
|
|
|
color=colors["text"],
|
|
|
stroke_width=4
|
|
|
)
|
|
|
h_label = MathTex("a",
|
|
|
color=colors["text"],
|
|
|
stroke_width=3,
|
|
|
stroke_color=BLACK).next_to(h_line, UP, buff=0.2)
|
|
|
|
|
|
self.play(
|
|
|
Create(v_line),
|
|
|
Write(v_label),
|
|
|
Create(h_line),
|
|
|
Write(h_label),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay)
|
|
|
|
|
|
# ========== 面积计算部分 ==========
|
|
|
part_a = Rectangle(
|
|
|
width=a,
|
|
|
height=a,
|
|
|
color=colors["a"],
|
|
|
fill_opacity=0.6
|
|
|
).move_to(axes.c2p(a/2, a/2))
|
|
|
|
|
|
part_b = Rectangle(
|
|
|
width=b,
|
|
|
height=a+b,
|
|
|
color=colors["b"],
|
|
|
fill_opacity=0.6
|
|
|
).move_to(axes.c2p(a + b/2, (a+b)/2))
|
|
|
|
|
|
self.play(
|
|
|
Transform(big_square, part_a),
|
|
|
FadeIn(part_b),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay)
|
|
|
|
|
|
# 移动并计算面积差
|
|
|
moved_part = part_b.copy().shift(DOWN*b)
|
|
|
area_diff = Rectangle(
|
|
|
width=b,
|
|
|
height=2*b,
|
|
|
color=RED,
|
|
|
fill_opacity=0.3
|
|
|
).move_to(axes.c2p(a + b/2, a - b))
|
|
|
|
|
|
self.play(
|
|
|
part_b.animate.shift(DOWN*b),
|
|
|
FadeIn(area_diff),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay)
|
|
|
|
|
|
# ========== 最终联动 ==========
|
|
|
final_formula = formula_steps[-1].copy()
|
|
|
final_formula.generate_target()
|
|
|
final_formula.target.scale(1.5).move_to(ORIGIN)
|
|
|
|
|
|
self.play(
|
|
|
MoveToTarget(final_formula),
|
|
|
FadeOut(axes),
|
|
|
FadeOut(part_a),
|
|
|
FadeOut(part_b),
|
|
|
FadeOut(area_diff),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(step_delay*2)
|
|
|
|
|
|
# 结束动画
|
|
|
self.play(
|
|
|
final_formula.animate.scale(0.5).to_edge(DOWN, buff=1),
|
|
|
run_time=1.5
|
|
|
)
|
|
|
self.wait(2)
|
|
|
|
|
|
# 版权信息
|
|
|
copyright = Text("© 东师理想 版权所有",
|
|
|
font_size=24,
|
|
|
color=colors["text"],
|
|
|
stroke_width=2,
|
|
|
stroke_color=BLACK).to_edge(DOWN, buff=0.3)
|
|
|
self.play(Write(copyright), run_time=1)
|
|
|
self.wait(3)
|
|
|
|
|
|
# 创建小熊角色(优化动画版)
|
|
|
bear = VGroup(
|
|
|
Circle(radius=0.5, color=GOLD, fill_opacity=1), # 头
|
|
|
Circle(radius=0.1, fill_opacity=1).shift(UR*0.3), # 右耳
|
|
|
Circle(radius=0.1, fill_opacity=1).shift(UL*0.3), # 左耳
|
|
|
Ellipse(width=0.8, height=1.2, fill_opacity=1).shift(DOWN*0.8) # 身体
|
|
|
).set_color([GOLD_E, LIGHT_BROWN])
|
|
|
bear.scale(0.8).to_corner(DL)
|
|
|
|
|
|
# 场景1:小熊出场
|
|
|
self.play(DrawBorderThenFill(bear), run_time=1.5)
|
|
|
self.play(
|
|
|
bear.animate.shift(UP*0.5).scale(1.2),
|
|
|
rate_func=there_and_back_with_pause,
|
|
|
run_time=1.5
|
|
|
)
|
|
|
|
|
|
# 创建对话气泡
|
|
|
bubble = self.create_bubble("大家好!今天我们来认识一个神奇的三角形")
|
|
|
bubble.next_to(bear, UP, buff=0.3)
|
|
|
self.play(
|
|
|
LaggedStart(
|
|
|
DrawBorderThenFill(bubble[0]),
|
|
|
DrawBorderThenFill(bubble[1]),
|
|
|
Write(bubble[2]),
|
|
|
lag_ratio=0.3
|
|
|
),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(2)
|
|
|
self.play(FadeOut(bubble))
|
|
|
|
|
|
# 创建3-4-5直角三角形
|
|
|
triangle = Polygon(
|
|
|
[-2, -1, 0], [2, -1, 0], [2, 2, 0],
|
|
|
color=WHITE,
|
|
|
fill_opacity=0.5,
|
|
|
stroke_width=4
|
|
|
)
|
|
|
labels = VGroup(
|
|
|
MathTex("3").next_to(triangle, LEFT, buff=0.3),
|
|
|
MathTex("4").next_to(triangle, DOWN, buff=0.3),
|
|
|
MathTex("5", color=YELLOW).next_to(triangle.get_center(), UR, buff=0.3)
|
|
|
)
|
|
|
|
|
|
self.play(DrawBorderThenFill(triangle), run_time=2)
|
|
|
self.play(LaggedStart(*[Write(l) for l in labels], lag_ratio=0.3))
|
|
|
self.play(
|
|
|
triangle.animate.shift(LEFT*2),
|
|
|
bear.animate.look_at(triangle),
|
|
|
run_time=1.5
|
|
|
)
|
|
|
self.wait()
|
|
|
|
|
|
# 场景2:构建正方形
|
|
|
square3 = Square(side_length=3, color=PINK, fill_opacity=0.3)
|
|
|
square4 = Square(side_length=4, color=BLUE, fill_opacity=0.3)
|
|
|
square5 = Square(side_length=5, color=GREEN, fill_opacity=0.3)
|
|
|
|
|
|
square3.next_to(triangle, LEFT, buff=0.5)
|
|
|
square4.next_to(triangle, DOWN, buff=0.5)
|
|
|
square5.rotate(np.arctan(3/4)).next_to(triangle, UR, buff=0.5)
|
|
|
|
|
|
self.play(
|
|
|
LaggedStart(
|
|
|
DrawBorderThenFill(square3),
|
|
|
DrawBorderThenFill(square4),
|
|
|
DrawBorderThenFill(square5),
|
|
|
lag_ratio=0.5
|
|
|
),
|
|
|
bear.animate.wave(rate_func=there_and_back),
|
|
|
run_time=3
|
|
|
)
|
|
|
|
|
|
# 添加面积标签
|
|
|
area_text = VGroup(
|
|
|
MathTex("3^2=9", color=PINK),
|
|
|
MathTex("4^2=16", color=BLUE),
|
|
|
MathTex("5^2=25", color=GREEN)
|
|
|
).arrange(DOWN, buff=0.5, aligned_edge=LEFT).to_edge(UR)
|
|
|
|
|
|
self.play(TransformFromCopy(square3, area_text[0]))
|
|
|
self.play(Indicate(square3, scale_factor=1.2))
|
|
|
self.play(TransformFromCopy(square4, area_text[1]))
|
|
|
self.play(Indicate(square4, scale_factor=1.2))
|
|
|
self.play(TransformFromCopy(square5, area_text[2]))
|
|
|
self.play(Indicate(square5, scale_factor=1.2))
|
|
|
self.wait()
|
|
|
|
|
|
# 场景3:组合动画
|
|
|
self.play(
|
|
|
square3.animate.shift(UP*2 + LEFT*1).rotate(PI/3),
|
|
|
square4.animate.shift(RIGHT*3 + DOWN*1).rotate(-PI/4),
|
|
|
bear.animate.blink(rate_func=there_and_back),
|
|
|
run_time=2
|
|
|
)
|
|
|
|
|
|
magic_text = Text("看!9 + 16 = 25", font="Microsoft YaHei", color=YELLOW).scale(0.8)
|
|
|
self.play(Write(magic_text))
|
|
|
self.play(
|
|
|
square3.animate.move_to(ORIGIN).set_opacity(0.3),
|
|
|
square4.animate.move_to(ORIGIN).set_opacity(0.3),
|
|
|
square5.animate.move_to(ORIGIN).set_opacity(0.3),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait()
|
|
|
|
|
|
# 场景4:公式推导
|
|
|
equation = MathTex("3^2 + 4^2 = 5^2", color=YELLOW).scale(1.5)
|
|
|
equation_box = SurroundingRectangle(equation, color=BLUE, buff=0.5, corner_radius=0.2)
|
|
|
self.play(
|
|
|
ReplacementTransform(magic_text, equation),
|
|
|
Create(equation_box),
|
|
|
bear.animate.jump(rate_func=there_and_back),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(2)
|
|
|
|
|
|
# 场景5:告别
|
|
|
bye_bubble = self.create_bubble("下次再见啦!")
|
|
|
bye_bubble.next_to(bear, UP, buff=0.3)
|
|
|
self.play(
|
|
|
FadeOut(VGroup(triangle, square3, square4, square5, equation_box, labels, area_text)),
|
|
|
LaggedStart(
|
|
|
DrawBorderThenFill(bye_bubble[0]),
|
|
|
DrawBorderThenFill(bye_bubble[1]),
|
|
|
Write(bye_bubble[2]),
|
|
|
lag_ratio=0.3
|
|
|
),
|
|
|
bear.animate.happy().scale(1.2),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.play(
|
|
|
bear.animate.rotate(PI/2, about_point=bear.get_center()).shift(DOWN*2),
|
|
|
run_time=2
|
|
|
)
|
|
|
self.wait(2)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
config.background_color = "#F0F8FF" # 浅蓝背景
|
|
|
config.frame_width = 16
|
|
|
config.frame_height = 9
|
|
|
config.pixel_width = 1280
|
|
|
config.pixel_height = 720
|
|
|
config.media_dir = "./output"
|
|
|
scene = SquareDifference()
|
|
|
scene.render() |