仅仅生成场景代码
思路
提取工具模块
- 将所有公共方法集中到
manim_utils.py
中。 - 将布局管理器独立到
layout_manager.py
中。
- 将所有公共方法集中到
场景脚本引用
在你的场景文件中,引入并调用上述两个模块:from manim_utils import custom_voiceover_tts from layout_manager import LayoutManager
生成指令约定
向大模型发送的提示中,明确说明「只需要生成场景脚本部分,不包含manim_utils.py
和layout_manager.py
的实现」。这样可以保证模型聚焦于场景构造代码。
公共方法抽单独的文件
manim_utils.py
# manim_utils.py
import os
import hashlib
import requests
from contextlib import contextmanager
from moviepy import AudioFileClip
import manimpango
from manim import config
from manim import RED, GREEN, BLUE, YELLOW, PURPLE, ORANGE, TEAL, PINK, WHITE,BLACK,DARK_GREY
# --- Font Detection ---
DEFAULT_FONT = "Noto Sans CJK SC"
FALLBACK_FONTS = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
def get_available_font():
"""Returns a font name if available, else None."""
available_fonts = manimpango.list_fonts()
if DEFAULT_FONT in available_fonts:
return DEFAULT_FONT
for font in FALLBACK_FONTS:
if font in available_fonts:
return font
return None
# --- TTS Caching Setup ---
CACHE_DIR = os.path.join(config.media_dir, "audio")
os.makedirs(CACHE_DIR, exist_ok=True)
class CustomVoiceoverTracker:
"""Tracks audio path and duration for TTS."""
def __init__(self, audio_path, duration):
self.audio_path = audio_path
self.duration = duration
def get_cache_filename(text: str) -> str:
"""Generates a unique filename based on the MD5 hash of the text."""
text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
return os.path.join(CACHE_DIR, f"{text_hash}.mp3")
@contextmanager
def custom_voiceover_tts(text: str,
token: str = "123456",
base_url: str = "https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches or uses cached TTS audio, yields a tracker with path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
duration = 0
if not os.path.exists(cache_file):
try:
encoded = requests.utils.quote(text)
url = f"{base_url}?token={token}&input={encoded}"
resp = requests.get(url, stream=True, timeout=60)
resp.raise_for_status()
with open(cache_file, 'wb') as f:
for chunk in resp.iter_content(8192):
if chunk:
f.write(chunk)
except Exception:
audio_file = None
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
except Exception:
audio_file = None
duration = 0
else:
audio_file = None
duration = 0
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass
# combined_scene.py
from manim import *
from manim_utils import (
get_available_font, custom_voiceover_tts,
WHITE, BLACK, BLUE,
GREEN, RED, YELLOW,
GRAY, DARK_GREY
)
# Set default font if available
final_font = get_available_font()
if final_font:
Text.set_default(font=final_font)
layout_manager.py
省略
使用示例
# -*- coding: utf-8 -*-
from manim import *
# Import the LayoutManager
from layout_manager import LayoutManager
# import custom_voiceover_tts
from manim_utils import custom_voiceover_tts
# --- Main Scene ---
class CombinedScene(Scene):
def setup(self):
super().setup()
# Initialize LayoutManager
self.layout_manager = LayoutManager(self)
self.current_scene_num_mob = None
self.section_elements = VGroup()
self.camera.background_color = WHITE
def update_scene_number(self, number_str: str):
# Use layout manager for the scene number text
new_num = Text(number_str, font_size=24)
self.layout_manager.register(new_num) # Will default to BLACK
new_num.to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
anims = [FadeIn(new_num, run_time=0.5)]
if self.current_scene_num_mob:
anims.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*anims)
self.current_scene_num_mob = new_num
def clear_section(self):
# No changes needed here, still operates on Mobjects
for mob in self.section_elements:
if hasattr(mob, 'clear_updaters'):
mob.clear_updaters()
if self.section_elements:
self.play(FadeOut(Group(*self.section_elements)), run_time=0.75)
self.section_elements = VGroup()
self.wait(0.1)
def construct(self):
"""Master construct method calling individual scene parts."""
self.play_scene_01()
self.clear_section()
self.play_scene_02()
self.clear_section()
self.play_scene_03()
self.clear_section()
self.play_scene_04()
self.clear_section()
self.play_scene_05()
self.clear_section()
# self.play_scene_06() # Assuming these are commented out intentionally
# self.clear_section()
# self.play_scene_07()
# No clear after the last scene
# --- Scene 1: Introduction ---
def play_scene_01(self):
self.update_scene_number("01")
# Title
title = Text("三角函数", font_size=48)
# Register title as important (bold, black)
self.layout_manager.register(title, important=True)
title.to_edge(UP, buff=0.8)
# Introduction text
intro_text = Text(
"三角函数是描述直角三角形中\n角度与边长关系的函数",
font_size=32,
line_spacing=1.2
)
# Register intro text (black)
self.layout_manager.register(intro_text)
intro_text.next_to(title, DOWN, buff=0.5)
# Right triangle
triangle = Polygon(
ORIGIN, RIGHT * 3, RIGHT * 3 + UP * 2,
# Keep explicit fill, stroke width. Register for stroke color.
fill_opacity=0.2,
stroke_width=2
)
# Register with specific stroke color (BLACK) and set fill manually
self.layout_manager.register(triangle, color=BLACK)
triangle.set_fill(BLUE, opacity=0.2) # Set fill color after registration
triangle.next_to(intro_text, DOWN, buff=0.6)
if triangle.get_bottom()[1] < -config.frame_height / 2 + 0.5:
triangle.shift(UP * (-config.frame_height / 2 + 0.5 - triangle.get_bottom()[1]))
# Triangle labels
angle_label = MathTex(r"\theta").scale(0.8)
hyp_label = MathTex(r"c").scale(0.8)
adj_label = MathTex(r"a").scale(0.8)
opp_label = MathTex(r"b").scale(0.8)
# Register labels (will be black)
self.layout_manager.register(angle_label)
self.layout_manager.register(hyp_label)
self.layout_manager.register(adj_label)
self.layout_manager.register(opp_label)
angle_label.next_to(triangle.get_vertices()[0] + RIGHT * 0.1 + UP * 0.1, UR, buff=0.1)
hyp_label.move_to(Line(triangle.get_vertices()[0], triangle.get_vertices()[2]).get_center() + UL * 0.3)
adj_label.next_to(Line(triangle.get_vertices()[0], triangle.get_vertices()[1]).get_center(), DOWN, buff=0.2)
opp_label.next_to(Line(triangle.get_vertices()[1], triangle.get_vertices()[2]).get_center(), RIGHT, buff=0.2)
right_angle = Square(side_length=0.3, stroke_width=2)
# Register right angle with specific color (GRAY)
self.layout_manager.register(right_angle, color=GRAY)
right_angle.move_to(triangle.get_vertices()[1], aligned_edge=DL)
# Group for clearing, no need to register the group itself if children are registered
triangle_group = VGroup(triangle, right_angle, angle_label, hyp_label, adj_label, opp_label)
self.section_elements.add(title, intro_text, triangle_group)
# Narration
voice_text_01 = "三角函数是数学中的一组重要函数,用于描述直角三角形中角度与边长之间的关系。在直角三角形中,我们有角θ,对边b,邻边a,和斜边c"
with custom_voiceover_tts(voice_text_01) as tracker:
# --- Animation remains the same ---
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Narration 1 TTS failed.")
self.play(FadeIn(title), run_time=1.0)
self.wait(0.5)
self.play(FadeIn(intro_text), run_time=1.5)
self.play(Create(triangle), run_time=1.5)
self.play(
Create(right_angle), Write(angle_label), Write(hyp_label),
Write(adj_label), Write(opp_label), run_time=2.0
)
anim_duration = 1.0 + 0.5 + 1.5 + 1.5 + 2.0
wait_time = max(0, tracker.duration - anim_duration) if tracker.duration > 0 else 2.0
self.wait(wait_time)
# --- Scene 2: Basic Definitions ---
def play_scene_02(self):
self.update_scene_number("02")
# Title
title = Text("基本三角函数", font_size=36)
# Register title as important
self.layout_manager.register(title, important=True)
title.to_edge(UP, buff=0.8)
# Definitions (Left Side)
def_scale = 0.9; def_buff = 0.6
# Create elements first, then register the containing VGroups
sine_lhs = MathTex(r"\sin \theta =").scale(def_scale)
sine_frac_text = VGroup(Text("对边"), Line(LEFT * 0.5, RIGHT * 0.5), Text("斜边")).arrange(DOWN, buff=0.15).scale(def_scale)
sine_eq1 = MathTex("=").scale(def_scale)
sine_frac_math = MathTex(r"\frac{b}{c}").scale(def_scale)
sine_def = VGroup(sine_lhs, sine_frac_text, sine_eq1, sine_frac_math).arrange(RIGHT, buff=0.2)
cosine_lhs = MathTex(r"\cos \theta =").scale(def_scale)
cosine_frac_text = VGroup(Text("邻边"), Line(LEFT * 0.5, RIGHT * 0.5), Text("斜边")).arrange(DOWN, buff=0.15).scale(def_scale)
cosine_eq1 = MathTex("=").scale(def_scale)
cosine_frac_math = MathTex(r"\frac{a}{c}").scale(def_scale)
cosine_def = VGroup(cosine_lhs, cosine_frac_text, cosine_eq1, cosine_frac_math).arrange(RIGHT, buff=0.2)
tangent_lhs = MathTex(r"\tan \theta =").scale(def_scale)
tangent_frac_text = VGroup(Text("对边"), Line(LEFT * 0.5, RIGHT * 0.5), Text("邻边")).arrange(DOWN, buff=0.15).scale(def_scale)
tangent_eq1 = MathTex("=").scale(def_scale)
tangent_frac_math = MathTex(r"\frac{b}{a}").scale(def_scale)
tangent_eq2 = MathTex("=").scale(def_scale)
tangent_frac_trig = MathTex(r"\frac{\sin \theta}{\cos \theta}").scale(def_scale)
tangent_def = VGroup(tangent_lhs, tangent_frac_text, tangent_eq1, tangent_frac_math, tangent_eq2, tangent_frac_trig).arrange(RIGHT, buff=0.2)
# Register the definition VGroups. Manager will handle children.
# Text/MathTex -> BLACK, Line -> Palette color
self.layout_manager.register(sine_def)
self.layout_manager.register(cosine_def)
self.layout_manager.register(tangent_def)
defs = VGroup(sine_def, cosine_def, tangent_def).arrange(DOWN, buff=def_buff, aligned_edge=LEFT)
defs.next_to(title, DOWN, buff=0.7).to_edge(LEFT, buff=1.0)
# Triangle (Right Side) - Reuse logic from Scene 1
triangle_scale = 0.8
triangle = Polygon(ORIGIN, RIGHT * 3, RIGHT * 3 + UP * 2, fill_opacity=0.2, stroke_width=2).scale(triangle_scale)
self.layout_manager.register(triangle, color=BLACK) # Stroke BLACK
triangle.set_fill(BLUE, opacity=0.2) # Fill BLUE
label_scale = 0.7 * triangle_scale
angle_label = MathTex(r"\theta").scale(label_scale)
hyp_label = MathTex(r"c").scale(label_scale)
adj_label = MathTex(r"a").scale(label_scale)
opp_label = MathTex(r"b").scale(label_scale)
self.layout_manager.register(angle_label)
self.layout_manager.register(hyp_label)
self.layout_manager.register(adj_label)
self.layout_manager.register(opp_label)
angle_label.next_to(triangle.get_vertices()[0] + RIGHT * 0.1 * triangle_scale + UP * 0.1 * triangle_scale, UR, buff=0.1)
hyp_label.move_to(Line(triangle.get_vertices()[0], triangle.get_vertices()[2]).get_center() + UL * 0.3 * triangle_scale)
adj_label.next_to(Line(triangle.get_vertices()[0], triangle.get_vertices()[1]).get_center(), DOWN, buff=0.2)
opp_label.next_to(Line(triangle.get_vertices()[1], triangle.get_vertices()[2]).get_center(), RIGHT, buff=0.2)
right_angle = Square(side_length=0.3 * triangle_scale, stroke_width=2)
self.layout_manager.register(right_angle, color=GRAY)
right_angle.move_to(triangle.get_vertices()[1], aligned_edge=DL)
triangle_group = VGroup(triangle, right_angle, angle_label, hyp_label, adj_label, opp_label)
triangle_group.to_edge(RIGHT, buff=1.5).align_to(defs, UP)
self.section_elements.add(title, defs, triangle_group)
# Narration
voice_text_02 = "基本的三角函数包括正弦、余弦和正切。正弦函数定义为对边除以斜边,余弦函数定义为邻边除以斜边,而正切函数定义为对边除以邻边,也等于正弦除以余弦。这些关系帮助我们在知道一个角和一条边时,计算三角形的其它边长。"
with custom_voiceover_tts(voice_text_02) as tracker:
# --- Animation logic remains the same ---
# Highlight lines are temporary, keep explicit colors, do not register
opp_line = Line(triangle.get_vertices()[1], triangle.get_vertices()[2], color=YELLOW, stroke_width=6)
hyp_line_sin = Line(triangle.get_vertices()[0], triangle.get_vertices()[2], color=RED, stroke_width=6)
adj_line = Line(triangle.get_vertices()[0], triangle.get_vertices()[1], color=YELLOW, stroke_width=6)
hyp_line_cos = Line(triangle.get_vertices()[0], triangle.get_vertices()[2], color=RED, stroke_width=6)
opp_line_tan = Line(triangle.get_vertices()[1], triangle.get_vertices()[2], color=YELLOW, stroke_width=6)
adj_line_tan = Line(triangle.get_vertices()[0], triangle.get_vertices()[1], color=RED, stroke_width=6)
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 2 TTS failed.")
self.play(FadeIn(title), run_time=1.0)
self.play(Create(triangle), run_time=1.0)
self.play(Create(right_angle), Write(angle_label), Write(hyp_label), Write(adj_label), Write(opp_label), run_time=1.5)
self.play(AnimationGroup(*[FadeIn(m) for m in sine_def], lag_ratio=0.1), run_time=1.5)
self.play(Create(opp_line), Create(hyp_line_sin), run_time=1.0); self.wait(1.0)
self.play(FadeOut(opp_line), FadeOut(hyp_line_sin), run_time=0.5)
self.play(AnimationGroup(*[FadeIn(m) for m in cosine_def], lag_ratio=0.1), run_time=1.5)
self.play(Create(adj_line), Create(hyp_line_cos), run_time=1.0); self.wait(1.0)
self.play(FadeOut(adj_line), FadeOut(hyp_line_cos), run_time=0.5)
self.play(AnimationGroup(*[FadeIn(m) for m in tangent_def], lag_ratio=0.1), run_time=2.0)
self.play(Create(opp_line_tan), Create(adj_line_tan), run_time=1.0); self.wait(1.5)
self.play(FadeOut(opp_line_tan), FadeOut(adj_line_tan), run_time=0.5)
anim_duration = (1.0 + 1.0 + 1.5 + 1.5 + 1.0 + 1.0 + 0.5 + 1.5 + 1.0 + 1.0 + 0.5 + 2.0 + 1.0 + 1.5 + 0.5)
wait_time = max(0, tracker.duration - anim_duration) if tracker.duration > 0 else 2.0
self.wait(wait_time)
# --- Scene 3: Unit Circle ---
def play_scene_03(self):
self.update_scene_number("03")
# Title
title = Text("单位圆与三角函数", font_size=36)
self.layout_manager.register(title, important=True) # Bold, Black
title.to_edge(UP, buff=0.8)
# --- Unit Circle Diagram (Left) ---
# DO NOT register axes to preserve detailed config
axes = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=5, y_length=5,
axis_config={"color": GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": False},
tips=False,
x_axis_config={"stroke_width": 2, "color": GRAY},
y_axis_config={"stroke_width": 2, "color": GRAY}
)
# Manually create labels for 1 and -1
x_labels_manual = VGroup(
MathTex("1", font_size=24).next_to(axes.c2p(1, 0), DOWN, buff=SMALL_BUFF),
MathTex("-1", font_size=24).next_to(axes.c2p(-1, 0), DOWN, buff=SMALL_BUFF)
)
y_labels_manual = VGroup(
MathTex("1", font_size=24).next_to(axes.c2p(0, 1), LEFT, buff=SMALL_BUFF),
MathTex("-1", font_size=24).next_to(axes.c2p(0, -1), LEFT, buff=SMALL_BUFF)
)
# Register the label groups (children become black)
self.layout_manager.register(x_labels_manual)
self.layout_manager.register(y_labels_manual)
axes_with_labels = VGroup(axes, x_labels_manual, y_labels_manual)
# Create unit circle
unit_radius_scene = axes.x_length / (axes.x_range[1] - axes.x_range[0]) * 1.0
circle = Circle(radius=unit_radius_scene, stroke_width=2)
self.layout_manager.register(circle, color=BLACK) # Explicitly black circle
circle.move_to(axes.c2p(0, 0))
# Angle tracker
theta = ValueTracker(30 * DEGREES)
# Point, radius, projections (with updaters)
# Register elements with their specific semantic colors
point = always_redraw(lambda: Dot(axes.c2p(np.cos(theta.get_value()), np.sin(theta.get_value()))))
radius = always_redraw(lambda: Line(axes.c2p(0, 0), point.get_center(), stroke_width=3))
x_proj = always_redraw(lambda: DashedLine(point.get_center(), axes.c2p(np.cos(theta.get_value()), 0), stroke_width=2))
y_proj = always_redraw(lambda: DashedLine(point.get_center(), axes.c2p(0, np.sin(theta.get_value())), stroke_width=2))
angle_arc = always_redraw(lambda: Arc(radius=0.4 * unit_radius_scene, angle=theta.get_value(), arc_center=axes.c2p(0, 0)))
angle_label = always_redraw(lambda: MathTex(r"\theta").scale(0.7).move_to(axes.c2p(0.5 * np.cos(theta.get_value() / 2), 0.5 * np.sin(theta.get_value() / 2))))
cos_label = always_redraw(lambda: MathTex(r"\cos \theta").scale(0.8).next_to(axes.c2p(np.cos(theta.get_value()), 0), DOWN, buff=SMALL_BUFF))
sin_label = always_redraw(lambda: MathTex(r"\sin \theta").scale(0.8).next_to(axes.c2p(0, np.sin(theta.get_value())), LEFT, buff=SMALL_BUFF))
self.layout_manager.register(point, color=YELLOW)
self.layout_manager.register(radius, color=RED)
self.layout_manager.register(x_proj, color=BLUE)
self.layout_manager.register(y_proj, color=GREEN)
self.layout_manager.register(angle_arc, color=YELLOW)
self.layout_manager.register(angle_label) # Black
self.layout_manager.register(cos_label, color=BLUE)
self.layout_manager.register(sin_label, color=GREEN)
# Group diagram elements and position on left
unit_circle_group = VGroup(axes_with_labels, circle, point, radius, x_proj, y_proj, angle_arc, angle_label, cos_label, sin_label)
unit_circle_group.center().to_edge(LEFT, buff=1.0)
# --- Explanation Text (Right Side) ---
expl_scale = 0.9; expl_buff = 0.4
expl_title = Text("在单位圆中:", font_size=28).scale(expl_scale)
# Keep specific colors for "y 坐标" and "x 坐标"
expl_sin = VGroup(MathTex(r"\sin \theta ="), Text("y 坐标", color=GREEN)).arrange(RIGHT, buff=0.1).scale(expl_scale)
expl_cos = VGroup(MathTex(r"\cos \theta ="), Text("x 坐标", color=BLUE)).arrange(RIGHT, buff=0.1).scale(expl_scale)
expl_tan = VGroup(MathTex(r"\tan \theta ="), MathTex(r"\frac{y}{x}"), MathTex(r"="), MathTex(r"\frac{\sin \theta}{\cos \theta}")).arrange(RIGHT, buff=0.1).scale(expl_scale)
explanation = VGroup(expl_title, expl_sin, expl_cos, expl_tan).arrange(DOWN, buff=expl_buff, aligned_edge=LEFT)
# Register the explanation group. Children MathTex/Text become black, except where overridden above.
self.layout_manager.register(explanation)
explanation.to_edge(RIGHT, buff=1.0).align_to(unit_circle_group, UP)
self.section_elements.add(title, unit_circle_group, explanation)
# Narration
voice_text_03 = "三角函数还可以通过单位圆来理解。在单位圆中,角θ对应圆上的一点,其 x 坐标就是余弦值,y 坐标就是正弦值。当点沿着单位圆移动时,正弦和余弦值随之变化。正切函数则等于 y 坐标除以 x 坐标。"
with custom_voiceover_tts(voice_text_03) as tracker:
# --- Animation logic remains the same ---
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 3 TTS failed.")
self.play(FadeIn(title), run_time=1.0)
self.play(Create(axes), Write(x_labels_manual), Write(y_labels_manual), Create(circle), run_time=1.5)
# Add registered elements that have updaters
self.add(point, radius, x_proj, y_proj, angle_arc, angle_label, cos_label, sin_label)
self.wait(0.5)
self.play(AnimationGroup(*[FadeIn(m) for m in explanation], lag_ratio=0.2), run_time=2.5)
self.play(theta.animate.set_value(390 * DEGREES), run_time=5, rate_func=linear)
self.wait(0.5)
self.play(theta.animate.set_value(30 * DEGREES), run_time=1.0)
anim_duration = 1.0 + 1.5 + 0.5 + 2.5 + 5.0 + 0.5 + 1.0
wait_time = max(0, tracker.duration - anim_duration) if tracker.duration > 0 else 1.0
self.wait(wait_time)
# --- Scene 4: Graphs ---
def play_scene_04(self):
self.update_scene_number("04")
# Title
title = Text("三角函数图像", font_size=36)
self.layout_manager.register(title, important=True) # Bold, Black
title.to_edge(UP, buff=0.8)
# DO NOT register axes to preserve detailed config
axes = Axes(
x_range=[0, 2 * PI + 0.1, PI / 2], y_range=[-1.5, 1.5, 1],
x_length=10, y_length=5,
axis_config={"color": GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": False},
tips=False,
x_axis_config={"stroke_width": 2, "color": GRAY},
y_axis_config={"stroke_width": 2, "color": GRAY}
)
# Manually create axis labels
x_labels_dict = { 0: r"0", PI / 2: r"\frac{\pi}{2}", PI: r"\pi", 3 * PI / 2: r"\frac{3\pi}{2}", 2 * PI: r"2\pi" }
y_labels_dict = {-1: r"-1", 1: r"1"}
x_labels_mobs = VGroup()
for x_val, tex_str in x_labels_dict.items():
label = MathTex(tex_str, font_size=24).next_to(axes.c2p(x_val, 0), DOWN, buff=SMALL_BUFF)
x_labels_mobs.add(label)
y_labels_mobs = VGroup()
for y_val, tex_str in y_labels_dict.items():
label = MathTex(tex_str, font_size=24).next_to(axes.c2p(0, y_val), LEFT, buff=SMALL_BUFF)
y_labels_mobs.add(label)
# Register label groups (children become black)
self.layout_manager.register(x_labels_mobs)
self.layout_manager.register(y_labels_mobs)
# Plot sine and cosine - register with specific colors
sin_graph = axes.plot(lambda x: np.sin(x), x_range=[0, 2 * PI])
cos_graph = axes.plot(lambda x: np.cos(x), x_range=[0, 2 * PI])
sin_label = MathTex(r"y = \sin \theta").scale(0.8).next_to(axes.c2p(2 * PI, np.sin(2 * PI)), UR, buff=0.2)
cos_label = MathTex(r"y = \cos \theta").scale(0.8).next_to(axes.c2p(2 * PI, np.cos(2 * PI)), DR, buff=0.2)
self.layout_manager.register(sin_graph, color=GREEN)
self.layout_manager.register(cos_graph, color=BLUE)
self.layout_manager.register(sin_label, color=GREEN)
self.layout_manager.register(cos_label, color=BLUE)
# Positioning
axes_with_labels = VGroup(axes, x_labels_mobs, y_labels_mobs)
graph_group = VGroup(axes_with_labels, sin_graph, cos_graph, sin_label, cos_label)
graph_group.next_to(title, DOWN, buff=0.5).center().shift(DOWN * 0.3)
self.section_elements.add(title, graph_group)
# Narration
voice_text_04 = "三角函数的图像展示了其周期性质。正弦函数图像类似波浪,从0开始,在π/2时达1,在π时回到0,在3π/2时降至-1,最后在2π回到0,如此循环。余弦函数则类似,但横向移动了π/2,从1开始,经0、-1,再回到1。这两函数都是周期为2π的周期函数。"
with custom_voiceover_tts(voice_text_04) as tracker:
# --- Animation logic remains the same ---
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 4 TTS failed.")
self.play(FadeIn(title), run_time=1.0)
self.play(Create(axes), Write(x_labels_mobs), Write(y_labels_mobs), run_time=2.0)
self.play(Create(sin_graph), run_time=2.5)
self.play(Write(sin_label), run_time=1.0); self.wait(1.0)
self.play(Create(cos_graph), run_time=2.5)
self.play(Write(cos_label), run_time=1.0); self.wait(1.0)
anim_duration = 1.0 + 2.0 + 2.5 + 1.0 + 1.0 + 2.5 + 1.0 + 1.0
wait_time = max(0, tracker.duration - anim_duration) if tracker.duration > 0 else 2.0
self.wait(wait_time)
# --- Scene 5: Identities ---
def play_scene_05(self):
self.update_scene_number("05")
# Title
title = Text("三角恒等式", font_size=36)
self.layout_manager.register(title, important=True) # Bold, Black
title.to_edge(UP, buff=0.8)
# Identities (Left Side)
identity_scale = 0.8
# Create the VGroup first
identities = VGroup(
MathTex(r"\sin^2 \theta + \cos^2 \theta = 1").scale(identity_scale),
MathTex(r"\sin(\alpha + \beta) = \sin \alpha \cos \beta + \cos \alpha \sin \beta").scale(identity_scale),
MathTex(r"\cos(\alpha + \beta) = \cos \alpha \cos \beta - \sin \alpha \sin \beta").scale(identity_scale),
MathTex(r"\sin(-\theta) = -\sin \theta").scale(identity_scale),
MathTex(r"\cos(-\theta) = \cos \theta").scale(identity_scale),
MathTex(r"\sin(\theta + 2\pi) = \sin \theta").scale(identity_scale),
MathTex(r"\cos(\theta + 2\pi) = \cos \theta").scale(identity_scale),
).arrange(DOWN, aligned_edge=LEFT, buff=0.3)
# Register the group (children become black)
self.layout_manager.register(identities)
identities.next_to(title, DOWN, buff=0.5).to_edge(LEFT, buff=1.0)
highlight_box = SurroundingRectangle(identities[0], buff=0.1, stroke_width=2)
# Register highlight box with specific color
self.layout_manager.register(highlight_box, color=YELLOW)
# Conclusion Text
conclusion = Text(
"这些恒等式在数学、物理、工程等领域有广泛应用",
font_size=28, width=config.frame_width - 2
)
# Register conclusion (black)
self.layout_manager.register(conclusion)
conclusion.to_edge(DOWN, buff=0.5)
self.section_elements.add(title, identities, highlight_box,conclusion)
# Narration
voice_text_05 = "三角函数有许多重要的恒等式。最基本的是勾股恒等式:sin²θ + cos²θ = 1。此外还有加法公式、负角公式和周期公式等。这些恒等式可以通过单位圆直观理解,并在数学、物理、工程等多个领域有广泛应用。"
with custom_voiceover_tts(voice_text_05) as tracker:
# --- Animation logic remains the same ---
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 5 TTS failed.")
self.play(FadeIn(title), run_time=1.0)
self.play(Write(identities[0]), run_time=1.5) # Animate first identity
self.play(Create(highlight_box), run_time=0.75); self.wait(1.0)
# Animate remaining identities
self.play(AnimationGroup(*[Write(identities[i]) for i in range(1, len(identities))], lag_ratio=0.2), run_time=4.0); self.wait(1.0)
self.play(FadeIn(conclusion), run_time=1.5)
anim_duration = 1.0 + 1.5 + 0.75 + 1.0 + 2.0 + 4.0 + 1.0 + 1.5
wait_time = max(0, tracker.duration - anim_duration) if tracker.duration > 0 else 2.0
self.wait(wait_time)