生成一个三角函数讲解视频
code
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
from moviepy import AudioFileClip # Correct import
import manimpango # For font checking
# --- Font Check --- (Ensure CJK font is available if needed, though not used here)
# DEFAULT_FONT = "Noto Sans CJK SC"
# available_fonts = manimpango.list_fonts()
# final_font = None
# if DEFAULT_FONT in available_fonts: final_font = DEFAULT_FONT
# else: print(f"Warning: Font '{DEFAULT_FONT}' not found.")
# Using Manim's default font for this example as it's primarily math/English.
# --- Custom Colors ---
MY_BACKGROUND = "#1a2a6c" # Dark Blue Gradient Start
MY_BACKGROUND_END = "#b21f1f" # Dark Red Gradient End (Used for simple bg now)
MY_CIRCLE_BLUE = "#4fc3f7" # Light Blue for Circle
MY_AXES_GRAY = "#cccccc" # Light Gray for Axes
MY_TEXT_WHITE = "#ffffff"
MY_TITLE_GOLD = "#ffd700" # Gold for Titles
MY_SINE_COLOR = "#ff69b4" # Pink for Sine
MY_COSINE_COLOR = "#add8e6" # Light Blue for Cosine
MY_TANGENT_COLOR = "#90ee90" # Light Green for Tangent
MY_RADIUS_RED = "#ff4d4d" # Red for Radius/Hypotenuse
MY_ANGLE_GREEN = "#32cd32" # Green for Angle
MY_HIGHLIGHT_YELLOW = "#ffff00" # Yellow for Highlights
MY_SUBTITLE_BG = "#000000" # Black for subtitle background
# --- TTS Caching Setup ---
CACHE_DIR = r"#(output_path)/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):
"""Generates a unique filename based on the text hash."""
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, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
"""Fetches TTS audio, caches it, and provides path and duration."""
cache_file = get_cache_filename(text)
audio_file = cache_file
if os.path.exists(cache_file):
# print(f"Using cached TTS for: {text[:30]}...")
pass # Use cached file
else:
# print(f"Requesting TTS for: {text[:30]}...")
try:
input_text_encoded = requests.utils.quote(text)
url = f"{base_url}?token={token}&input={input_text_encoded}"
response = requests.get(url, stream=True, timeout=60)
response.raise_for_status()
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: f.write(chunk)
audio_file = cache_file
# print("TTS downloaded and cached.")
except requests.exceptions.RequestException as e:
print(f"TTS API request failed: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
except Exception as e:
print(f"An error occurred during TTS processing: {e}")
if os.path.exists(cache_file): os.remove(cache_file) # Clean up partial file
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return
# Get duration
duration = 0
if audio_file and os.path.exists(audio_file):
try:
with AudioFileClip(audio_file) as clip:
duration = clip.duration
# print(f"Audio duration: {duration:.2f}s")
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
audio_file = None
duration = 0
else:
# print(f"TTS audio file not found or not created: {audio_file}")
audio_file = None
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass # Keep cache
# -----------------------------
# CombinedScene: Explaining Trigonometry
# -----------------------------
class CombinedScene(MovingCameraScene):
"""
Explains Sine, Cosine, and Tangent using the unit circle and right triangles.
"""
def setup(self):
MovingCameraScene.setup(self)
# Set default font if needed and checked
# if final_font: Text.set_default(font=final_font)
self.theta_tracker = ValueTracker(PI / 6) # Start angle
self.current_scene_num_mob = None
# Store elements shared within a scene part if needed (e.g., axes reference)
self.axes = None
self.unit_circle_visuals = VGroup() # Group for circle, radius, point etc.
self.triangle_visuals = VGroup() # Group for triangle lines and labels
self.formula_visuals = VGroup() # Group for formula displays
def update_scene_number(self, number_str):
"""Fades out the old scene number and fades in the new one."""
new_scene_num = Text(number_str, font_size=24, color=MY_TEXT_WHITE).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
animations = [FadeIn(new_scene_num, run_time=0.5)]
if self.current_scene_num_mob:
animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
self.play(*animations)
self.current_scene_num_mob = new_scene_num
def clear_and_reset(self):
"""Clears all objects and resets camera and trackers."""
# Clear updaters first
mobjects_to_clear = list(self.mobjects)
for mob in mobjects_to_clear:
if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
mob.clear_updaters()
# Fade out all valid mobjects
valid_mobjects = [m for m in self.mobjects if m is not None]
if valid_mobjects:
self.play(FadeOut(Group(*valid_mobjects)), run_time=0.5)
self.clear() # Clears self.mobjects
# Reset camera
self.camera.frame.move_to(ORIGIN)
self.camera.frame.set(width=config.frame_width, height=config.frame_height)
# Reset trackers
self.theta_tracker.set_value(PI / 6) # Reset to start angle
# Clear stored groups and references
self.axes = None
self.unit_circle_visuals = VGroup()
self.triangle_visuals = VGroup()
self.formula_visuals = VGroup()
self.wait(0.1)
def create_unit_circle_base(self, position=ORIGIN, scale=1.0):
"""Creates the axes and unit circle."""
self.axes = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1],
x_length=6 * scale, y_length=6 * scale,
axis_config={"color": MY_AXES_GRAY, "include_tip": False, "stroke_width": 2, "include_numbers": True},
x_axis_config={"numbers_to_include": [-1, 1]},
y_axis_config={"numbers_to_include": [-1, 1]},
tips=False
).move_to(position)
x_label = self.axes.get_x_axis_label("x", edge=RIGHT, direction=RIGHT, buff=SMALL_BUFF)
# Position y-label to the left to avoid top overlap
y_label = self.axes.get_y_axis_label("y", edge=LEFT, direction=LEFT, buff=MED_SMALL_BUFF)
axes_labels = VGroup(x_label, y_label).set_color(MY_AXES_GRAY)
radius_val = 1.0
origin_point = self.axes.c2p(0, 0)
radius_point = self.axes.c2p(radius_val, 0)
screen_radius = np.linalg.norm(radius_point - origin_point)
circle = Circle(radius=screen_radius, color=MY_CIRCLE_BLUE, stroke_width=3, arc_center=origin_point)
return VGroup(self.axes, axes_labels, circle)
def create_dynamic_elements(self, radius_val=1.0):
"""Creates elements that depend on theta_tracker."""
origin_point = self.axes.c2p(0, 0)
radius_point = self.axes.c2p(radius_val, 0)
screen_radius = np.linalg.norm(radius_point - origin_point)
radius_line = always_redraw(
lambda: Line(
self.axes.c2p(0, 0),
self.axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), radius_val * np.sin(self.theta_tracker.get_value())),
color=MY_RADIUS_RED, stroke_width=4
)
)
p_dot = always_redraw(
lambda: Dot(
self.axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), radius_val * np.sin(self.theta_tracker.get_value())),
color=MY_RADIUS_RED, radius=0.08
)
)
p_label = always_redraw(
lambda: MathTex("P(x, y)", color=MY_TEXT_WHITE, font_size=30).next_to(p_dot.get_center(), UR, buff=SMALL_BUFF)
)
theta_arc = always_redraw(
lambda: Arc(
radius=0.4 * screen_radius,
start_angle=0,
angle=self.theta_tracker.get_value(),
color=MY_ANGLE_GREEN,
arc_center=self.axes.c2p(0, 0)
)
)
theta_label = always_redraw(
lambda: MathTex(r"\theta", color=MY_ANGLE_GREEN, font_size=36).move_to(
self.axes.c2p(0,0) + Arc(radius=0.6 * screen_radius, angle=self.theta_tracker.get_value()).point_from_proportion(0.5)
)
)
radius_label_text = MathTex("r=1", color=MY_RADIUS_RED, font_size=30)
radius_label = always_redraw(
lambda: radius_label_text.next_to(radius_line.get_center(), UP+LEFT, buff=SMALL_BUFF)
)
dynamic_group = VGroup(radius_line, p_dot, p_label, theta_arc, theta_label, radius_label)
# Add elements with updaters directly to the scene
self.add(radius_line, p_dot, p_label, theta_arc, theta_label, radius_label)
return dynamic_group
def create_subtitle(self, text):
"""Creates a subtitle with background."""
subtitle_text = Text(
text, font_size=28, color=MY_TEXT_WHITE,
width=config.frame_width - 2, should_center=True
)
# Correct: Create background and apply styles to it
subtitle_bg = SurroundingRectangle(
subtitle_text, buff=0.15, color=MY_SUBTITLE_BG,
fill_color=MY_SUBTITLE_BG, fill_opacity=0.7, stroke_width=0
)
subtitle_group = VGroup(subtitle_bg, subtitle_text)
subtitle_group.to_edge(DOWN, buff=MED_SMALL_BUFF)
return subtitle_group
def construct(self):
# --- Scene Setup ---
bg = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_BACKGROUND, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
# Simple background for now, gradient requires more setup
self.add(bg)
# --- Play Scenes ---
self.play_scene_01()
self.clear_and_reset()
self.play_scene_02()
self.clear_and_reset()
self.play_scene_03()
self.clear_and_reset()
self.play_scene_04()
self.clear_and_reset()
self.play_scene_05()
# Final wait handled in scene 5
# --- Scene 1: Introduction to Unit Circle ---
def play_scene_01(self):
"""Scene 1: Introduces the unit circle and basic components."""
self.update_scene_number("01")
title = Text("Trigonometry & The Unit Circle", font_size=48, color=MY_TITLE_GOLD).to_edge(UP, buff=MED_LARGE_BUFF)
base_elements = self.create_unit_circle_base()
dynamic_elements = self.create_dynamic_elements()
self.unit_circle_visuals.add(base_elements, dynamic_elements)
# Narration 1
voice_text_01 = "Welcome! Today we'll explore the fundamental trigonometric functions: Sine, Cosine, and Tangent, using the Unit Circle. The unit circle has a radius of 1 and is centered at the origin (0,0). A point P moves along the circle, forming an angle theta with the positive x-axis."
with custom_voiceover_tts(voice_text_01) as tracker:
subtitle_voice = self.create_subtitle(voice_text_01)
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 1 TTS failed.")
# Animation
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(Create(base_elements), run_time=2.0)
self.play(Create(dynamic_elements), run_time=2.0)
# Animate angle change
self.play(self.theta_tracker.animate.set_value(5 * PI / 4), run_time=3.0, rate_func=smooth)
self.play(self.theta_tracker.animate.set_value(PI / 3), run_time=2.0, rate_func=smooth)
# Wait for narration
anim_duration = 1.0 + 2.0 + 2.0 + 3.0 + 2.0
wait_time = max(0, tracker.duration - anim_duration - 0.5)
if wait_time > 0: self.wait(wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Scene 2: Sine (sin θ) ---
def play_scene_02(self):
"""Scene 2: Defines Sine using the unit circle and triangle."""
self.update_scene_number("02")
title = Text("Sine (sin θ)", font_size=48, color=MY_SINE_COLOR).to_edge(UP, buff=MED_LARGE_BUFF)
# Recreate visuals
base_elements = self.create_unit_circle_base()
dynamic_elements = self.create_dynamic_elements()
self.unit_circle_visuals.add(base_elements, dynamic_elements)
self.add(base_elements) # Add static parts
# Sine specific elements
p_dot = dynamic_elements[1] # Get the p_dot from dynamic elements
vert_line = always_redraw(
lambda: DashedLine(
p_dot.get_center(),
self.axes.c2p(0, np.sin(self.theta_tracker.get_value())), # Project to y-axis
color=MY_SINE_COLOR, stroke_width=2
)
)
y_coord_line = always_redraw(
lambda: Line(
self.axes.c2p(0, 0),
self.axes.c2p(0, np.sin(self.theta_tracker.get_value())),
color=MY_SINE_COLOR, stroke_width=4
)
)
sine_label = always_redraw(
lambda: MathTex(r"\sin \theta", color=MY_SINE_COLOR, font_size=36).next_to(
self.axes.c2p(0, np.sin(self.theta_tracker.get_value()) / 2), # Midpoint of y-line
LEFT, buff=MED_SMALL_BUFF
)
)
sine_elements = VGroup(vert_line, y_coord_line, sine_label)
self.add(vert_line, y_coord_line, sine_label) # Add updaters
# Right Triangle
triangle = always_redraw(lambda: Polygon(
self.axes.c2p(0, 0),
self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0),
p_dot.get_center(),
stroke_width=0, fill_opacity=0 # Invisible, just for labels
))
opp_side = always_redraw(lambda: Line(self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0), p_dot.get_center(), color=MY_SINE_COLOR, stroke_width=4))
hyp_side = dynamic_elements[0] # Radius line is hypotenuse
opp_label = always_redraw(lambda: MathTex("y = \\sin \\theta", font_size=30, color=MY_SINE_COLOR).next_to(opp_side.get_center(), RIGHT, buff=SMALL_BUFF))
hyp_label = always_redraw(lambda: MathTex("r=1", font_size=30, color=MY_RADIUS_RED).next_to(hyp_side.get_center(), UP+LEFT, buff=SMALL_BUFF))
self.triangle_visuals.add(triangle, opp_side, opp_label, hyp_label) # Add opp_side for redraw
self.add(opp_side, opp_label, hyp_label) # Add updaters
# Formulas
unit_circle_def = MathTex(r"\sin \theta = y", font_size=40, color=MY_SINE_COLOR)
triangle_def = MathTex(r"\sin \theta = \frac{\text{Opposite}}{\text{Hypotenuse}} = \frac{y}{1}", font_size=40, color=MY_SINE_COLOR)
self.formula_visuals.add(unit_circle_def, triangle_def).arrange(DOWN, buff=MED_LARGE_BUFF).to_edge(RIGHT, buff=LARGE_BUFF)
# Narration 2
voice_text_02 = "Sine, written as sin theta, is defined as the y-coordinate of point P on the unit circle. As theta changes, the y-coordinate changes, tracing the sine value. In the right-angled triangle formed inside the circle, sine is the ratio of the length of the side opposite the angle theta to the length of the hypotenuse."
with custom_voiceover_tts(voice_text_02) as tracker:
subtitle_voice = self.create_subtitle(voice_text_02)
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 2 TTS failed.")
# Animation
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(Create(vert_line), Create(y_coord_line), FadeIn(sine_label), run_time=2.0)
self.play(self.theta_tracker.animate.set_value(2 * PI / 3), run_time=2.0)
self.play(Create(opp_side), FadeIn(opp_label), Indicate(hyp_label), run_time=2.0) # Show triangle sides
self.play(Write(unit_circle_def), run_time=1.5)
self.play(Write(triangle_def), run_time=2.0)
self.play(self.theta_tracker.animate.set_value(PI / 4), run_time=2.0) # Move angle again
# Wait for narration
anim_duration = 1.0 + 2.0 + 2.0 + 2.0 + 1.5 + 2.0 + 2.0
wait_time = max(0, tracker.duration - anim_duration - 0.5)
if wait_time > 0: self.wait(wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Scene 3: Cosine (cos θ) ---
def play_scene_03(self):
"""Scene 3: Defines Cosine using the unit circle and triangle."""
self.update_scene_number("03")
title = Text("Cosine (cos θ)", font_size=48, color=MY_COSINE_COLOR).to_edge(UP, buff=MED_LARGE_BUFF)
# Recreate visuals
base_elements = self.create_unit_circle_base()
dynamic_elements = self.create_dynamic_elements()
self.unit_circle_visuals.add(base_elements, dynamic_elements)
self.add(base_elements)
# Cosine specific elements
p_dot = dynamic_elements[1]
horz_line = always_redraw(
lambda: DashedLine(
p_dot.get_center(),
self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0), # Project to x-axis
color=MY_COSINE_COLOR, stroke_width=2
)
)
x_coord_line = always_redraw(
lambda: Line(
self.axes.c2p(0, 0),
self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0),
color=MY_COSINE_COLOR, stroke_width=4
)
)
cosine_label = always_redraw(
lambda: MathTex(r"\cos \theta", color=MY_COSINE_COLOR, font_size=36).next_to(
self.axes.c2p(np.cos(self.theta_tracker.get_value()) / 2, 0), # Midpoint of x-line
DOWN, buff=MED_SMALL_BUFF
)
)
cosine_elements = VGroup(horz_line, x_coord_line, cosine_label)
self.add(horz_line, x_coord_line, cosine_label)
# Right Triangle
triangle = always_redraw(lambda: Polygon(
self.axes.c2p(0, 0),
self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0),
p_dot.get_center(),
stroke_width=0, fill_opacity=0
))
adj_side = x_coord_line # Adjacent side is the x-coord line
hyp_side = dynamic_elements[0] # Radius line
adj_label = always_redraw(lambda: MathTex("x = \\cos \\theta", font_size=30, color=MY_COSINE_COLOR).next_to(adj_side.get_center(), DOWN, buff=SMALL_BUFF))
hyp_label = always_redraw(lambda: MathTex("r=1", font_size=30, color=MY_RADIUS_RED).next_to(hyp_side.get_center(), UP+LEFT, buff=SMALL_BUFF))
self.triangle_visuals.add(triangle, adj_label, hyp_label) # Add adj_label for redraw
self.add(adj_label, hyp_label)
# Formulas
unit_circle_def = MathTex(r"\cos \theta = x", font_size=40, color=MY_COSINE_COLOR)
triangle_def = MathTex(r"\cos \theta = \frac{\text{Adjacent}}{\text{Hypotenuse}} = \frac{x}{1}", font_size=40, color=MY_COSINE_COLOR)
self.formula_visuals.add(unit_circle_def, triangle_def).arrange(DOWN, buff=MED_LARGE_BUFF).to_edge(RIGHT, buff=LARGE_BUFF)
# Narration 3
voice_text_03 = "Cosine, or cos theta, is the x-coordinate of point P. As theta changes, the x-coordinate changes, giving the cosine value. In our right triangle, cosine is the ratio of the length of the side adjacent to angle theta to the length of the hypotenuse."
with custom_voiceover_tts(voice_text_03) as tracker:
subtitle_voice = self.create_subtitle(voice_text_03)
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 3 TTS failed.")
# Animation
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(Create(horz_line), Create(x_coord_line), FadeIn(cosine_label), run_time=2.0)
self.play(self.theta_tracker.animate.set_value(5 * PI / 6), run_time=2.0)
self.play(Indicate(adj_side), FadeIn(adj_label), Indicate(hyp_label), run_time=2.0) # Show triangle sides
self.play(Write(unit_circle_def), run_time=1.5)
self.play(Write(triangle_def), run_time=2.0)
self.play(self.theta_tracker.animate.set_value(PI / 6), run_time=2.0) # Move angle again
# Wait for narration
anim_duration = 1.0 + 2.0 + 2.0 + 2.0 + 1.5 + 2.0 + 2.0
wait_time = max(0, tracker.duration - anim_duration - 0.5)
if wait_time > 0: self.wait(wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Scene 4: Tangent (tan θ) ---
def play_scene_04(self):
"""Scene 4: Defines Tangent using the unit circle and triangle."""
self.update_scene_number("04")
title = Text("Tangent (tan θ)", font_size=48, color=MY_TANGENT_COLOR).to_edge(UP, buff=MED_LARGE_BUFF)
# Recreate visuals
base_elements = self.create_unit_circle_base()
dynamic_elements = self.create_dynamic_elements()
self.unit_circle_visuals.add(base_elements, dynamic_elements)
self.add(base_elements)
# Tangent specific elements
p_dot = dynamic_elements[1]
radius_line = dynamic_elements[0]
# Geometric tangent line segment
tangent_line_geom = always_redraw(lambda:
Line(
self.axes.c2p(1, 0), # Start at (1,0) on circle
self.axes.c2p(1, np.tan(self.theta_tracker.get_value())), # End at x=1, y=tan(theta)
color=MY_TANGENT_COLOR, stroke_width=4
)
)
# Extend radius line to meet the tangent line
extended_radius = always_redraw(lambda:
Line(
self.axes.c2p(0,0),
self.axes.c2p(1, np.tan(self.theta_tracker.get_value())), # End where tangent line ends
color=MY_RADIUS_RED, stroke_width=1, stroke_opacity=0.5 # Fainter extended part
)
)
tangent_label = always_redraw(lambda:
MathTex(r"\tan \theta", color=MY_TANGENT_COLOR, font_size=36).next_to(
tangent_line_geom.get_center(), RIGHT, buff=MED_SMALL_BUFF
)
)
tangent_elements = VGroup(tangent_line_geom, extended_radius, tangent_label)
self.add(tangent_line_geom, extended_radius, tangent_label)
# Right Triangle (Original one inside circle)
opp_side = always_redraw(lambda: Line(self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0), p_dot.get_center(), color=MY_SINE_COLOR, stroke_width=2, stroke_opacity=0.7))
adj_side = always_redraw(lambda: Line(self.axes.c2p(0, 0), self.axes.c2p(np.cos(self.theta_tracker.get_value()), 0), color=MY_COSINE_COLOR, stroke_width=2, stroke_opacity=0.7))
opp_label = always_redraw(lambda: MathTex("y", font_size=24, color=MY_SINE_COLOR).next_to(opp_side.get_center(), RIGHT, buff=SMALL_BUFF))
adj_label = always_redraw(lambda: MathTex("x", font_size=24, color=MY_COSINE_COLOR).next_to(adj_side.get_center(), DOWN, buff=SMALL_BUFF))
self.triangle_visuals.add(opp_side, adj_side, opp_label, adj_label)
self.add(opp_side, adj_side, opp_label, adj_label)
# Formulas
ratio_def = MathTex(r"\tan \theta = \frac{y}{x} = \frac{\sin \theta}{\cos \theta}", font_size=40, color=MY_TANGENT_COLOR)
triangle_def = MathTex(r"\tan \theta = \frac{\text{Opposite}}{\text{Adjacent}}", font_size=40, color=MY_TANGENT_COLOR)
self.formula_visuals.add(ratio_def, triangle_def).arrange(DOWN, buff=MED_LARGE_BUFF).to_edge(RIGHT, buff=LARGE_BUFF)
# Narration 4
voice_text_04 = "Finally, Tangent, or tan theta. It's defined as the ratio of sine to cosine, or y divided by x. Geometrically, it represents the length of the vertical line segment from the x-axis up to the point where the extended radius intersects the vertical line at x equals 1. In the right triangle, tangent is the ratio of the Opposite side to the Adjacent side."
with custom_voiceover_tts(voice_text_04) as tracker:
subtitle_voice = self.create_subtitle(voice_text_04)
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 4 TTS failed.")
# Animation
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
# Show triangle sides first for context
self.play(Create(opp_side), Create(adj_side), FadeIn(opp_label), FadeIn(adj_label), run_time=1.5)
# Show geometric tangent
self.play(Create(extended_radius), Create(tangent_line_geom), FadeIn(tangent_label), run_time=2.5)
self.play(self.theta_tracker.animate.set_value(PI / 3), run_time=2.0) # Move angle
# Show formulas
self.play(Write(ratio_def), run_time=2.0)
self.play(Write(triangle_def), run_time=2.0)
self.play(self.theta_tracker.animate.set_value(PI / 5), run_time=2.0) # Move angle again
# Wait for narration
anim_duration = 1.0 + 1.5 + 2.5 + 2.0 + 2.0 + 2.0 + 2.0
wait_time = max(0, tracker.duration - anim_duration - 0.5)
if wait_time > 0: self.wait(wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(1)
# --- Scene 5: SOH CAH TOA & Summary ---
def play_scene_05(self):
"""Scene 5: Introduces SOH CAH TOA and summarizes definitions."""
self.update_scene_number("05")
title = Text("Summary: SOH CAH TOA", font_size=48, color=MY_TITLE_GOLD).to_edge(UP, buff=MED_LARGE_BUFF)
# SOH CAH TOA Mnemonic
soh = MathTex(r"\text{SOH: } \sin \theta = \frac{\text{Opposite}}{\text{Hypotenuse}}", color=MY_SINE_COLOR)
cah = MathTex(r"\text{CAH: } \cos \theta = \frac{\text{Adjacent}}{\text{Hypotenuse}}", color=MY_COSINE_COLOR)
toa = MathTex(r"\text{TOA: } \tan \theta = \frac{\text{Opposite}}{\text{Adjacent}}", color=MY_TANGENT_COLOR)
mnemonic_group = VGroup(soh, cah, toa).arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT).scale(0.9)
# Unit Circle Definitions Summary
unit_circle_summary_title = Text("Unit Circle Definitions:", font_size=36, color=MY_TEXT_WHITE)
sin_unit = MathTex(r"\sin \theta = y", color=MY_SINE_COLOR)
cos_unit = MathTex(r"\cos \theta = x", color=MY_COSINE_COLOR)
tan_unit = MathTex(r"\tan \theta = y/x", color=MY_TANGENT_COLOR)
unit_defs_group = VGroup(sin_unit, cos_unit, tan_unit).arrange(DOWN, buff=MED_SMALL_BUFF, aligned_edge=LEFT).scale(0.9)
unit_summary_group = VGroup(unit_circle_summary_title, unit_defs_group).arrange(DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
# Arrange groups side-by-side
summary_content = VGroup(mnemonic_group, unit_summary_group).arrange(RIGHT, buff=LARGE_BUFF * 1.5)
summary_content.next_to(title, DOWN, buff=LARGE_BUFF)
# Conclusion Text
conclusion = Text("Mastering these definitions is key to trigonometry!", font_size=32, color=MY_TEXT_WHITE)
conclusion.to_edge(DOWN, buff=MED_LARGE_BUFF)
# Narration 5
voice_text_05 = "To easily remember the triangle definitions, use the mnemonic SOH CAH TOA: Sine is Opposite over Hypotenuse, Cosine is Adjacent over Hypotenuse, and Tangent is Opposite over Adjacent. Remember, these also correspond directly to the y, x, and y/x coordinates on the unit circle. Mastering these definitions is key to trigonometry! Thanks for watching."
with custom_voiceover_tts(voice_text_05) as tracker:
subtitle_voice = self.create_subtitle(voice_text_05)
if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
else: print("Warning: Narration 5 TTS failed.")
# Animation
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(Write(mnemonic_group), run_time=3.0)
self.play(FadeIn(unit_summary_group), run_time=2.5)
self.play(FadeIn(conclusion), run_time=1.5)
# Wait for narration
anim_duration = 1.0 + 3.0 + 2.5 + 1.5
wait_time = max(0, tracker.duration - anim_duration - 0.5)
if wait_time > 0: self.wait(wait_time)
self.play(FadeOut(subtitle_voice), run_time=0.5)
self.wait(3) # Hold final screen longer
# Fade out scene number at the very end
if self.current_scene_num_mob:
self.play(FadeOut(self.current_scene_num_mob))
# --- Main execution block ---
if __name__ == "__main__":
# Basic configuration
config.pixel_height = 1080
config.pixel_width = 1920
config.frame_rate = 30
config.output_file = "CombinedScene"
config.disable_caching = True
# Set output directory using placeholder
config.media_dir = r"#(output_path)" # Use raw string
# Create and render the scene
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")
video
https://manim.collegebot.ai/cache/499940685498376192/videos/1080p30/CombinedScene.mp4