generate a video from unit circle to cos
Topic
generate a video from unit circle to cos
Sennce
## From Unit Circle to Cosine: A Visual Explanation
**Overall Animation Style:** Clean, modern, with a focus on clarity and intuitive visualization. Utilize color coding effectively. The animation should smoothly transition between concepts.
---
### **【Scene 1: Introduction to the Unit Circle】**
* **Background:** Solid light gray (e.g., `#f0f0f0`).
* **Elements:**
* **Unit Circle:** A circle centered at the origin (0, 0) with a radius of 1. Draw it with a crisp, clean line (e.g., a thicker line weight for emphasis). Color: Blue (#007bff).
* **Coordinate Axes:** X and Y axes intersecting at the origin. Label them "x" and "y" in a subtle font. Axis color: Dark gray (#555555). Range: Extend the axes slightly beyond the circle (e.g., -1.5 to 1.5 on both x and y).
* **Radius:** A line segment from the origin to a point *P* on the circle. Make it initially horizontal along the positive x-axis. Color: Red (#dc3545). Label it "r = 1" near the line segment.
* **Point P:** A small, filled circle at the end of the radius on the unit circle. Color: Red (#dc3545). Label it *P* near the point.
* **Angle θ (Theta):** An arc indicating the angle between the positive x-axis and the radius. Color: Green (#28a745). Label it "θ" (using LaTeX) near the arc. The arc should be visually distinct and easy to follow.
* **Text:** At the top of the screen, display: "The Unit Circle". Color: Black (#000000).
* **Animation:**
1. **Coordinate Axes Create:** Animate the x and y axes coming into existence from the origin.
2. **Unit Circle Grow:** Animate the unit circle expanding from the origin to its full size.
3. **Radius Draw:** Draw the radius from the origin to point *P*.
4. **Angle θ Form:** Animate the angle arc appearing, starting from the x-axis and extending to the radius.
5. **Labels Fade In:** Fade in the labels "r = 1", "θ", and "P".
6. **Title Fade In:** Fade in the title "The Unit Circle".
* **Camera:** Start with the camera centered on the origin and the circle fully in view. No camera movement initially.
---
### **【Scene 2: Introducing Cosine as the X-Coordinate】**
* **Background:** Solid light gray (#f0f0f0), the same as Scene 1. The unit circle, axes, point P, and angle θ remain visible from the previous scene.
* **Elements:**
* **Vertical Line (Drop):** A vertical dashed line segment from point *P* down to the x-axis. Color: Orange (#ffc107).
* **Point on X-Axis:** A small, filled circle where the vertical line intersects the x-axis. Color: Orange (#ffc107).
* **Label "cos θ":** Place the label "cos θ" (using LaTeX) below the point on the x-axis, indicating the x-coordinate. Color: Orange (#ffc107).
* **Coordinate Labels:** Display the coordinates of point *P* as "(cos θ, sin θ)" (using LaTeX) near point *P*. Color: Red (#dc3545) for the entire coordinate pair.
* **Text:** Display the following text near the top of the screen: "Cosine (cos θ) is the x-coordinate of point P on the unit circle." Color: Black (#000000).
* **Animation:**
1. **Vertical Line Create:** Animate the vertical dashed line segment dropping from point *P* to the x-axis.
2. **X-Axis Point Create:** Animate the small circle appearing on the x-axis at the intersection.
3. **"cos θ" Label Fade In:** Fade in the label "cos θ" below the x-axis point.
4. **Coordinate Labels Fade In:** Fade in the coordinate labels "(cos θ, sin θ)" near point *P*.
5. **Text Fade In:** Fade in the explanatory text at the top.
6. **Radius Rotate:** Smoothly rotate the radius counter-clockwise. The vertical dashed line, the point on the x-axis, and the "cos θ" label should move in sync with the radius, showing the changing x-coordinate. Rotate for a duration of at least 5 seconds, covering a significant range of angles (e.g., 0 to π).
* **Camera:** The camera should remain centered on the unit circle, following the rotation of the radius, so that the relevant changes are always in view.
---
### **【Scene 3: Graphing Cosine vs. Angle】**
* **Background:** Solid light gray (#f0f0f0).
* **Elements:**
* **Unit Circle (Smaller):** A smaller version of the unit circle from the previous scenes, positioned on the *left* side of the screen. It should still have the radius, point *P*, vertical line, point on x-axis, angle θ arc, and the "cos θ" label.
* **Coordinate System (Graph):** A new coordinate system on the *right* side of the screen. X-axis label: "θ" (angle in radians). Y-axis label: "cos θ". X-axis range: 0 to 2π (approximately). Y-axis range: -1.2 to 1.2.
* **Cosine Graph:** A curve that plots the value of "cos θ" (from the unit circle) against the angle "θ" on the right-hand graph. As the radius rotates on the unit circle, a point should move along the cosine curve, tracing it out. Color: Orange (#ffc107) - matching the "cos θ" label.
* **Animation:**
1. **Unit Circle Slide In:** Slide the smaller unit circle into position on the left side.
2. **Graph Axes Create:** Animate the x and y axes of the graph coming into existence on the right side.
3. **Graph Draw:**
* Rotate the radius on the unit circle slowly and smoothly.
* Simultaneously, a point on the graph should move horizontally based on the angle θ of the rotating radius and vertically based on the "cos θ" value on the unit circle. The point should leave a trail, forming the cosine graph. Use `Create` animation for this curve and `Rotating` for the radius.
4. **Sync the Animations:** The key is to perfectly synchronize the radius rotation with the graph plotting so that viewers can clearly see the direct correspondence. The rate of rotation and graphing should be slow enough for understanding but fast enough to avoid boredom.
* **Camera:** Start with a wide shot showing both the unit circle and the empty graph. As the cosine graph is being drawn, the camera can subtly zoom in on the graph to emphasize the shape of the curve.
---
### **【Scene 4: Highlighting Key Features of the Cosine Graph】**
* **Background:** Solid light gray (#f0f0f0). The unit circle and cosine graph remain visible from the previous scene.
* **Elements:**
* **Vertical Lines:** Vertical dashed lines on the cosine graph at key angles: 0, π/2, π, 3π/2, and 2π. Color: Light gray (#aaaaaa). Label each line with the corresponding angle (using LaTeX).
* **Horizontal Lines:** Horizontal dashed lines on the cosine graph at y = 1, y = 0, and y = -1. Color: Light gray (#aaaaaa). Label each line with the corresponding y-value.
* **Highlighting:** Rectangles or circles briefly highlighting key points on the graph (maxima, minima, zeros). Color: A contrasting color like purple (#800080), but semi-transparent so the graph underneath remains visible.
* **Arrows:** Arrows pointing from specific points on the unit circle (e.g., (1,0), (0,1), (-1,0), (0,-1)) to the corresponding points on the cosine graph. Color: The same color as the highlighting rectangles (purple #800080).
* **Text:** Display the following text near the top of the screen: "Key Values and Properties of Cosine". Color: Black (#000000).
* **Animation:**
1. **Vertical/Horizontal Lines Create:** Animate the vertical and horizontal dashed lines appearing on the graph.
2. **Labels Fade In:** Fade in the labels for the angles and y-values.
3. **Highlighting Pulsate:** Briefly pulsate the highlighting rectangles/circles around key points on the graph. Use the `Glow` animation.
4. **Arrows Draw:** Draw the arrows connecting points on the unit circle to points on the graph.
* **Camera:** The camera can slowly pan across the graph, following the highlighting and arrows, to guide the viewer's attention to different features.
---
### **【Scene 5: Conclusion】**
* **Background:** Dark blue (#001f3f) for a more dramatic concluding effect.
* **Elements:**
* **Unit Circle and Cosine Graph (Faded):** Faded versions of the unit circle and cosine graph from the previous scenes, placed side-by-side.
* **Final Text:** Display the following text prominently in the center of the screen: "Cosine: The x-coordinate on the Unit Circle!" Color: White (#ffffff). Use a slightly larger font size.
* **Thanks Text:** Display "Thank You!" below the final text. Color: Light gray (#cccccc).
* **Animation:**
1. **Unit Circle and Graph Fade Out:** Fade out the active colors of the unit circle and cosine graph, leaving them in a muted state.
2. **Final Text Fade In:** Fade in the final concluding text.
3. **Thanks Text Fade In:** Fade in the "Thank You!" text.
* **Camera:** Start with a wide shot showing the faded unit circle and graph. Slightly zoom in on the final text to emphasize the conclusion. Add a very slow, subtle rotation for a more dynamic feel.
---
**General Notes:**
* **Timing:** Careful attention to timing is crucial. Animations should be smooth and paced to allow viewers to understand the connections. Use `Wait` to pause between animation steps.
* **Color Consistency:** Maintain consistent color coding throughout the animation to help viewers track the relationships between elements.
* **LaTeX:** Ensure all mathematical formulas (angles, coordinates, etc.) are rendered using LaTeX for clarity and professional appearance.
* **Code Optimization:** Optimize the Manim code for efficiency and readability. Use functions to create reusable components.
* **Narrative:** Consider adding a voiceover or text narration to guide viewers through the animation and explain the concepts.
* **Easing:** Use easing functions (e.g., `smooth`, `linear`, `ease_in_out`) to make the animations more visually appealing.
This detailed prompt should provide a solid foundation for creating a clear, engaging, and informative Manim animation explaining the relationship between the unit circle and the cosine function. Remember to test and refine the animation as you build it to ensure the visuals accurately and effectively communicate the concepts. Good luck!
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
# --- Custom Colors ---
MY_LIGHT_GRAY = "#f0f0f0"
MY_DARK_GRAY = "#555555"
MY_BLUE = "#007bff"
MY_RED = "#dc3545"
MY_GREEN = "#28a745"
MY_ORANGE = "#ffc107" # Matches cos θ
MY_PURPLE = "#800080" # Highlighting
MY_BLACK = "#000000"
MY_WHITE = "#ffffff"
MY_CONCLUSION_BG = "#001f3f" # Dark blue for conclusion
MY_CONCLUSION_FG = "#ffffff"
MY_CONCLUSION_SUB = "#cccccc"
# --- TTS Caching Setup ---
CACHE_DIR = "#(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}")
# If duration fails, treat as no audio
audio_file = None
duration = 0
else:
# print(f"TTS audio file not found or not created: {audio_file}")
audio_file = None # Ensure audio_path is None if file doesn't exist
tracker = CustomVoiceoverTracker(audio_file, duration)
try:
yield tracker
finally:
pass # Keep cache
# -----------------------------
# CombinedScene: Unit Circle to Cosine Graph
# -----------------------------
class CombinedScene(MovingCameraScene):
"""
Visually explains the connection between the unit circle and the cosine function.
"""
def setup(self):
MovingCameraScene.setup(self)
# Optional: Set default font here if checked
# if final_font: Text.set_default(font=final_font)
# Initialize theta tracker for animations involving angle
self.theta_tracker = ValueTracker(0)
# Store elements that need to be accessed across animations within a scene part
self.unit_circle_elements = VGroup()
self.graph_elements = VGroup()
def construct(self):
# --- Play Scenes Sequentially ---
self.play_scene_01()
self.clear_and_reset() # Clear before starting next scene
self.play_scene_02()
self.clear_and_reset() # Clear before starting next scene (Needed for Scene 3 layout)
self.play_scene_03()
self.clear_and_reset() # <<--- ADDED: Clear before Scene 4 to prevent overlap
self.play_scene_04()
self.clear_and_reset() # Clear before conclusion
self.play_scene_05()
# Final wait is handled in play_scene_05
def get_scene_number(self, number_str, color=MY_BLACK):
"""Creates and positions the scene number."""
scene_num = Text(number_str, font_size=24, color=color) # Allow color change
# Increase buffer to avoid overlap with potential titles near the top edge
scene_num.to_corner(UR, buff=MED_LARGE_BUFF) # Use a larger buffer
scene_num.set_z_index(10)
return scene_num
def clear_and_reset(self):
"""Clears all objects and resets camera and trackers."""
# Clear updaters from all mobjects first
mobjects_to_clear = list(self.mobjects) # Make a copy
for mob in mobjects_to_clear:
# Check if mob exists and has the get_updaters method
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 any camera rotation if needed (MovingCameraScene uses frame properties)
# self.camera.frame.set_theta(0) # Example if rotation was used
# Reset trackers
self.theta_tracker.set_value(0)
# Clear stored groups
self.unit_circle_elements = VGroup()
self.graph_elements = VGroup()
self.wait(0.1)
# --- Scene 1: Introduction to the Unit Circle ---
def play_scene_01(self):
"""Scene 1: Introduces the unit circle and its components."""
# Background
bg1 = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_LIGHT_GRAY, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
self.add(bg1)
# Scene Number
scene_num_01 = self.get_scene_number("01", color=MY_BLACK)
self.add(scene_num_01)
# Title
title = Text("The Unit Circle", font_size=48, color=MY_BLACK).to_edge(UP, buff=MED_LARGE_BUFF)
# Coordinate Axes
axes = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1],
x_length=6, y_length=6,
axis_config={"color": MY_DARK_GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": True},
x_axis_config={"numbers_to_include": [-1, 1]},
y_axis_config={"numbers_to_include": [-1, 1]},
tips=False
)
x_label = axes.get_x_axis_label("x", edge=RIGHT, direction=RIGHT, buff=SMALL_BUFF)
# --- MODIFIED Y LABEL POSITION ---
y_label = axes.get_y_axis_label("y", edge=LEFT, direction=LEFT, buff=MED_SMALL_BUFF) # Position left of axis top
axes_labels = VGroup(x_label, y_label).set_color(MY_DARK_GRAY)
# Unit Circle
radius_val = 1.0
origin_point = axes.c2p(0, 0)
radius_point = axes.c2p(radius_val, 0)
screen_radius = np.linalg.norm(radius_point - origin_point)
circle = Circle(radius=screen_radius, color=MY_BLUE, stroke_width=3, arc_center=origin_point)
# Radius, Point P, Angle Theta
self.theta_tracker.set_value(PI / 4)
radius = always_redraw(
lambda: Line(
axes.c2p(0, 0),
axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), radius_val * np.sin(self.theta_tracker.get_value())),
color=MY_RED, stroke_width=3
)
)
p_dot = always_redraw(
lambda: Dot(
axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), radius_val * np.sin(self.theta_tracker.get_value())),
color=MY_RED, radius=0.08
)
)
p_label = always_redraw(
lambda: MathTex("P", color=MY_RED, font_size=36).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_GREEN,
arc_center=axes.c2p(0, 0)
)
)
theta_label = always_redraw(
lambda: MathTex(r"\theta", color=MY_GREEN, font_size=36).move_to(
axes.c2p(0,0) + Arc(radius=0.6 * screen_radius, angle=self.theta_tracker.get_value()).point_from_proportion(0.5)
)
)
radius_label = always_redraw(
lambda: MathTex("r=1", color=MY_RED, font_size=30).next_to(radius.get_center(), UR, buff=SMALL_BUFF)
)
# Group elements
self.unit_circle_elements.add(axes, axes_labels, circle, radius, p_dot, p_label, theta_arc, theta_label, radius_label)
self.add(radius, p_dot, p_label, theta_arc, theta_label, radius_label) # Add updaters
# --- TTS ---
voice_text_01 = "Let's start with the unit circle. This is a circle centered at the origin with a radius of exactly one. We have our standard x and y axes. A point P moves along the circle. The line connecting the origin to P is the radius, which always has length 1. The angle between the positive x-axis and this radius is called theta."
with custom_voiceover_tts(voice_text_01) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Scene 1 TTS failed.")
subtitle_voice = Text(voice_text_01, font_size=28, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
# --- Animation ---
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.play(Create(axes), Write(axes_labels), run_time=1.5)
self.play(GrowFromCenter(circle), run_time=1.5)
self.play(Create(radius), Create(p_dot), FadeIn(p_label), run_time=1.0)
self.play(Create(theta_arc), FadeIn(theta_label), run_time=1.0)
self.play(FadeIn(radius_label), run_time=0.5)
# Wait
anim_duration = 1.0 + 1.5 + 1.5 + 1.0 + 1.0 + 0.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(1)
# --- Scene 2: Introducing Cosine as the X-Coordinate ---
def play_scene_02(self):
"""Scene 2: Defines cosine as the x-coordinate on the unit circle."""
# Background
bg2 = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_LIGHT_GRAY, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
self.add(bg2)
# Scene Number
scene_num_02 = self.get_scene_number("02", color=MY_BLACK)
self.add(scene_num_02)
# Recreate Unit Circle elements
axes = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=6, y_length=6,
axis_config={"color": MY_DARK_GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": True},
x_axis_config={"numbers_to_include": [-1, 1]}, y_axis_config={"numbers_to_include": [-1, 1]},
tips=False
)
x_label = axes.get_x_axis_label("x", edge=RIGHT, direction=RIGHT, buff=SMALL_BUFF).set_color(MY_DARK_GRAY)
# --- MODIFIED Y LABEL POSITION ---
y_label = axes.get_y_axis_label("y", edge=LEFT, direction=LEFT, buff=MED_SMALL_BUFF).set_color(MY_DARK_GRAY) # Position left of axis top
axes_labels = VGroup(x_label, y_label)
radius_val = 1.0
origin_point = axes.c2p(0, 0)
radius_point = axes.c2p(radius_val, 0)
screen_radius = np.linalg.norm(radius_point - origin_point)
circle = Circle(radius=screen_radius, color=MY_BLUE, stroke_width=3, arc_center=origin_point)
self.theta_tracker.set_value(0)
radius = always_redraw(lambda: Line(axes.c2p(0, 0), axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), radius_val * np.sin(self.theta_tracker.get_value())), color=MY_RED, stroke_width=3))
p_dot = always_redraw(lambda: Dot(axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), radius_val * np.sin(self.theta_tracker.get_value())), color=MY_RED, radius=0.08))
p_label = always_redraw(lambda: MathTex("P", color=MY_RED, font_size=36).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_GREEN, arc_center=axes.c2p(0, 0)))
theta_label = always_redraw(lambda: MathTex(r"\theta", color=MY_GREEN, font_size=36).move_to(axes.c2p(0,0) + Arc(radius=0.6 * screen_radius, angle=self.theta_tracker.get_value()).point_from_proportion(0.5)))
self.add(axes, axes_labels, circle)
self.add(radius, p_dot, p_label, theta_arc, theta_label)
# New elements
vert_line = always_redraw(
lambda: DashedLine(
p_dot.get_center(),
axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), 0),
color=MY_ORANGE, stroke_width=2
)
)
x_intersect_dot = always_redraw(
lambda: Dot(axes.c2p(radius_val * np.cos(self.theta_tracker.get_value()), 0), color=MY_ORANGE, radius=0.06)
)
cos_label = always_redraw(
lambda: MathTex(r"\cos \theta", color=MY_ORANGE, font_size=36).next_to(x_intersect_dot.get_center(), DOWN, buff=MED_SMALL_BUFF)
)
coord_label = always_redraw(
lambda: MathTex(r"(\cos \theta, \sin \theta)", color=MY_RED, font_size=36).next_to(p_dot.get_center(), RIGHT, buff=MED_SMALL_BUFF)
)
explanation_text = Text("Cosine (cos θ) is the x-coordinate of point P on the unit circle.",
font_size=36, color=MY_BLACK).to_edge(UP, buff=MED_LARGE_BUFF)
self.add(vert_line, x_intersect_dot, cos_label, coord_label) # Add updaters
# --- TTS ---
voice_text_02 = "Now, let's focus on the coordinates of point P. If we drop a vertical line from P down to the x-axis, the x-coordinate of this intersection point is defined as the cosine of the angle theta, written as cos theta. The y-coordinate is the sine of theta. So, the coordinates of P are (cos theta, sin theta). Watch how the x-coordinate, cos theta, changes as the angle theta increases."
with custom_voiceover_tts(voice_text_02) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Scene 2 TTS failed.")
subtitle_voice = Text(voice_text_02, font_size=28, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
# --- Animation ---
self.play(FadeIn(explanation_text), FadeIn(subtitle_voice), run_time=1.0)
self.play(Create(vert_line), Create(x_intersect_dot), run_time=1.0)
self.play(FadeIn(cos_label), FadeIn(coord_label), run_time=1.0)
rotate_duration = 5.0
self.play(self.theta_tracker.animate.set_value(PI), run_time=rotate_duration, rate_func=linear)
# Wait
anim_duration = 1.0 + 1.0 + 1.0 + rotate_duration
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)
# No need to store elements as clear_and_reset is called next
self.wait(1)
# --- Scene 3: Graphing Cosine vs. Angle ---
def play_scene_03(self):
"""Scene 3: Connects the unit circle's x-coordinate to the cosine graph."""
# Background
bg3 = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_LIGHT_GRAY, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
self.add(bg3)
# Scene Number
scene_num_03 = self.get_scene_number("03", color=MY_BLACK)
self.add(scene_num_03)
# --- Left Side: Unit Circle (Smaller) ---
axes_unit = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=4, y_length=4,
axis_config={"color": MY_DARK_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
)
radius_val_unit = 1.0
origin_point_unit = axes_unit.c2p(0, 0)
radius_point_unit = axes_unit.c2p(radius_val_unit, 0)
screen_radius_unit = np.linalg.norm(radius_point_unit - origin_point_unit)
circle_unit = Circle(radius=screen_radius_unit, color=MY_BLUE, stroke_width=3, arc_center=origin_point_unit)
self.theta_tracker.set_value(0)
radius_unit = always_redraw(lambda: Line(axes_unit.c2p(0, 0), axes_unit.c2p(radius_val_unit * np.cos(self.theta_tracker.get_value()), radius_val_unit * np.sin(self.theta_tracker.get_value())), color=MY_RED, stroke_width=3))
p_dot_unit = always_redraw(lambda: Dot(axes_unit.c2p(radius_val_unit * np.cos(self.theta_tracker.get_value()), radius_val_unit * np.sin(self.theta_tracker.get_value())), color=MY_RED, radius=0.06))
theta_arc_unit = always_redraw(lambda: Arc(radius=0.3 * screen_radius_unit, start_angle=0, angle=self.theta_tracker.get_value(), color=MY_GREEN, arc_center=axes_unit.c2p(0, 0)))
vert_line_unit = always_redraw(lambda: DashedLine(p_dot_unit.get_center(), axes_unit.c2p(radius_val_unit * np.cos(self.theta_tracker.get_value()), 0), color=MY_ORANGE, stroke_width=2))
x_intersect_dot_unit = always_redraw(lambda: Dot(axes_unit.c2p(radius_val_unit * np.cos(self.theta_tracker.get_value()), 0), color=MY_ORANGE, radius=0.05))
cos_label_unit = always_redraw(lambda: MathTex(r"\cos \theta", color=MY_ORANGE, font_size=30).next_to(x_intersect_dot_unit.get_center(), DOWN, buff=MED_SMALL_BUFF))
unit_circle_group_small = VGroup(axes_unit, circle_unit, radius_unit, p_dot_unit, theta_arc_unit, vert_line_unit, x_intersect_dot_unit, cos_label_unit)
unit_circle_group_small.scale(0.8).to_edge(LEFT, buff=LARGE_BUFF)
self.add(radius_unit, p_dot_unit, theta_arc_unit, vert_line_unit, x_intersect_dot_unit, cos_label_unit) # Add updaters
# --- Right Side: Cosine Graph ---
axes_graph = Axes(
x_range=[0, 2 * PI + 0.1, PI / 2],
y_range=[-1.2, 1.2, 1],
x_length=8, y_length=4,
axis_config={"color": MY_DARK_GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": True},
x_axis_config={"include_numbers": False},
y_axis_config={"numbers_to_include": [-1, 0, 1]},
tips=False
)
# Manually add Pi labels for x-axis
x_labels_pi = VGroup()
custom_x_values_labels = {
0: "0",
PI/2: r"\pi/2",
PI: r"\pi",
3*PI/2: r"3\pi/2",
2*PI: r"2\pi"
}
for x_val, label_tex in custom_x_values_labels.items():
label = MathTex(label_tex, font_size=24, color=MY_DARK_GRAY)
label.next_to(axes_graph.c2p(x_val, 0), DOWN, buff=MED_SMALL_BUFF)
x_labels_pi.add(label)
x_graph_label = axes_graph.get_x_axis_label(r"\theta", edge=DOWN, direction=DOWN, buff=MED_SMALL_BUFF).set_color(MY_DARK_GRAY)
y_graph_label = axes_graph.get_y_axis_label(r"\cos \theta", edge=LEFT, direction=LEFT, buff=MED_SMALL_BUFF).set_color(MY_DARK_GRAY)
axes_graph_labels = VGroup(x_graph_label, y_graph_label)
graph_group = VGroup(axes_graph, axes_graph_labels, x_labels_pi).to_edge(RIGHT, buff=LARGE_BUFF)
cosine_graph = axes_graph.plot(lambda x: np.cos(x), x_range=[0, 2 * PI], color=MY_ORANGE, stroke_width=3)
moving_dot_graph = always_redraw(
lambda: Dot(
axes_graph.input_to_graph_point(self.theta_tracker.get_value(), cosine_graph),
color=MY_RED, radius=0.08
)
)
connecting_line = always_redraw(
lambda: DashedLine(
x_intersect_dot_unit.get_center(),
moving_dot_graph.get_center(),
stroke_width=1, color=MY_DARK_GRAY, dash_length=0.05
)
)
# Group graph elements (add cosine_graph here)
self.graph_elements.add(graph_group, cosine_graph, moving_dot_graph, connecting_line)
self.add(moving_dot_graph, connecting_line) # Add updaters
# --- TTS ---
voice_text_03 = "Now, let's visualize how the cosine value relates to its graph. On the left, we have our unit circle. On the right, we'll plot the angle theta on the horizontal axis and the value of cos theta (the x-coordinate from the unit circle) on the vertical axis. As the angle theta increases from 0 to 2 pi on the unit circle, watch how the corresponding cos theta value traces out the familiar cosine wave on the graph."
with custom_voiceover_tts(voice_text_03) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Scene 3 TTS failed.")
subtitle_voice = Text(voice_text_03, font_size=28, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
# --- Animation ---
self.play(FadeIn(subtitle_voice), run_time=0.5)
self.play(FadeIn(unit_circle_group_small, shift=RIGHT), Create(graph_group), run_time=2.0)
self.add(moving_dot_graph, connecting_line)
graph_creation_duration = 6.0
self.play(
self.theta_tracker.animate.set_value(2 * PI),
Create(cosine_graph),
run_time=graph_creation_duration,
rate_func=linear
)
# Wait
anim_duration = 0.5 + 2.0 + graph_creation_duration
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)
# Don't store elements as clear_and_reset is called next
self.wait(1)
# --- Scene 4: Highlighting Key Features ---
def play_scene_04(self):
"""Scene 4: Highlights key points (max, min, zeros) on the cosine graph."""
# Background
bg4 = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_LIGHT_GRAY, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
self.add(bg4)
# Scene Number
scene_num_04 = self.get_scene_number("04", color=MY_BLACK)
self.add(scene_num_04)
# --- Recreate elements needed for this scene ---
# Unit Circle (Left)
axes_unit = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=4, y_length=4,
axis_config={"color": MY_DARK_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
)
radius_val_unit = 1.0
origin_point_unit = axes_unit.c2p(0, 0)
radius_point_unit = axes_unit.c2p(radius_val_unit, 0)
screen_radius_unit = np.linalg.norm(radius_point_unit - origin_point_unit)
circle_unit = Circle(radius=screen_radius_unit, color=MY_BLUE, stroke_width=3, arc_center=origin_point_unit)
unit_circle_group_small = VGroup(axes_unit, circle_unit).scale(0.8).to_edge(LEFT, buff=LARGE_BUFF)
self.unit_circle_elements = unit_circle_group_small # Store for this scene
# Cosine Graph (Right)
axes_graph = Axes(
x_range=[0, 2 * PI + 0.1, PI / 2],
y_range=[-1.2, 1.2, 1],
x_length=8, y_length=4,
axis_config={"color": MY_DARK_GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": True},
x_axis_config={"include_numbers": False},
y_axis_config={"numbers_to_include": [-1, 0, 1]},
tips=False
)
x_labels_pi = VGroup()
custom_x_values_labels = {
0: "0", PI/2: r"\pi/2", PI: r"\pi", 3*PI/2: r"3\pi/2", 2*PI: r"2\pi"
}
for x_val, label_tex in custom_x_values_labels.items():
label = MathTex(label_tex, font_size=24, color=MY_DARK_GRAY)
label.next_to(axes_graph.c2p(x_val, 0), DOWN, buff=MED_SMALL_BUFF)
x_labels_pi.add(label)
x_graph_label = axes_graph.get_x_axis_label(r"\theta", edge=DOWN, direction=DOWN, buff=MED_SMALL_BUFF).set_color(MY_DARK_GRAY)
y_graph_label = axes_graph.get_y_axis_label(r"\cos \theta", edge=LEFT, direction=LEFT, buff=MED_SMALL_BUFF).set_color(MY_DARK_GRAY)
axes_graph_labels = VGroup(x_graph_label, y_graph_label)
graph_group = VGroup(axes_graph, axes_graph_labels, x_labels_pi).to_edge(RIGHT, buff=LARGE_BUFF)
cosine_graph_obj = axes_graph.plot(lambda x: np.cos(x), x_range=[0, 2 * PI], color=MY_ORANGE, stroke_width=3)
self.graph_elements = VGroup(graph_group, cosine_graph_obj) # Store for this scene
# Add recreated elements to scene
self.add(self.unit_circle_elements, self.graph_elements)
# Title
title = Text("Key Values and Properties of Cosine", font_size=36, color=MY_BLACK).to_edge(UP, buff=MED_LARGE_BUFF)
# Key points calculation
key_angles = [0, PI / 2, PI, 3 * PI / 2, 2 * PI]
key_values = [1, 0, -1, 0, 1]
if not cosine_graph_obj.has_points():
print("Error: cosine_graph_obj has no points in Scene 4!")
return
key_points_graph_coords = [axes_graph.input_to_graph_point(angle, cosine_graph_obj) for angle in key_angles]
# Vertical Lines and Labels
v_lines = VGroup()
v_labels = VGroup()
existing_x_labels = {lbl.tex_string: lbl for lbl in x_labels_pi} # Use dict for lookup
for angle in key_angles:
line = axes_graph.get_vertical_line(axes_graph.i2gp(angle, cosine_graph_obj), color=MY_DARK_GRAY, stroke_width=1, line_func=DashedLine)
label_text_raw = custom_x_values_labels.get(angle, "") # Get label text from dict
if label_text_raw:
label = existing_x_labels.get(label_text_raw)
if label is None:
print(f"Warning: Could not find existing label for {label_text_raw}, creating new.")
label = MathTex(label_text_raw, font_size=24, color=MY_DARK_GRAY).next_to(axes_graph.c2p(angle, 0), DOWN, buff=MED_SMALL_BUFF)
v_labels.add(label)
v_lines.add(line)
# Horizontal Lines and Labels
h_lines = VGroup()
h_labels = VGroup()
for val in [-1, 0, 1]:
line = axes_graph.get_horizontal_line(axes_graph.c2p(0, val), color=MY_DARK_GRAY, stroke_width=1, line_func=DashedLine)
label = axes_graph.get_y_axis().get_number_mobject(val)
if label is None:
label = MathTex(str(val), font_size=24, color=MY_DARK_GRAY)
label.next_to(axes_graph.c2p(0, val), LEFT, buff=MED_SMALL_BUFF)
else:
label.next_to(axes_graph.c2p(0, val), LEFT, buff=MED_SMALL_BUFF)
h_lines.add(line)
h_labels.add(label)
# Highlighting Key Points
highlights = VGroup()
for point_coord in key_points_graph_coords:
highlight = Circle(radius=0.15, color=MY_PURPLE, fill_opacity=0.3, stroke_width=0).move_to(point_coord)
highlights.add(highlight)
# Arrows
unit_circle_points_coords = [
axes_unit.c2p(radius_val_unit * np.cos(angle), radius_val_unit * np.sin(angle)) for angle in key_angles
]
arrows = VGroup()
for i, angle in enumerate(key_angles):
start_point = unit_circle_points_coords[i]
end_point = key_points_graph_coords[i]
if isinstance(start_point, np.ndarray) and isinstance(end_point, np.ndarray) and \
not np.any(np.isnan(start_point)) and not np.any(np.isnan(end_point)) and \
not np.any(np.isinf(start_point)) and not np.any(np.isinf(end_point)):
arrow = Arrow(start_point, end_point, buff=0.1, color=MY_PURPLE, stroke_width=2, max_tip_length_to_length_ratio=0.1)
arrows.add(arrow)
else:
print(f"Warning: Skipping arrow {i} due to invalid coordinates.")
# --- TTS ---
voice_text_04 = "Let's examine some key features. When theta is 0, cos theta is 1, the maximum value. At pi/2, cos theta is 0. At pi, cos theta reaches its minimum, -1. At 3 pi/2, it's 0 again. And at 2 pi, it returns to 1, completing one cycle. These points correspond directly to the x-coordinates on the unit circle at those angles."
with custom_voiceover_tts(voice_text_04) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Scene 4 TTS failed.")
subtitle_voice = Text(voice_text_04, font_size=28, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
# --- Animation ---
self.play(FadeIn(title), FadeIn(subtitle_voice), run_time=1.0)
self.add(v_labels, h_labels) # Ensure labels are added before animation
self.play(Create(v_lines), Create(h_lines), FadeIn(v_labels), FadeIn(h_labels), run_time=2.0)
highlight_anims = []
for i in range(len(highlights)):
highlight_anims.append(Indicate(highlights[i], color=MY_PURPLE, scale_factor=1.5, run_time=0.8))
if i < len(arrows):
highlight_anims.append(Create(arrows[i], run_time=0.8))
highlight_anims.append(Wait(0.3))
if highlight_anims:
self.play(Succession(*highlight_anims))
# Wait
highlight_duration = len(highlights) * (0.8 + 0.3) + len(arrows) * 0.8
anim_duration = 1.0 + 2.0 + highlight_duration
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: Conclusion ---
def play_scene_05(self):
"""Scene 5: Concludes the explanation."""
# Background
bg5 = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_CONCLUSION_BG, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
self.add(bg5)
# Scene Number
scene_num_05 = self.get_scene_number("05", color=MY_CONCLUSION_SUB)
self.add(scene_num_05)
# --- Recreate faded elements for conclusion ---
# Unit Circle (Left, Faded)
axes_unit_faded = Axes(
x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=4, y_length=4,
axis_config={"color": MY_DARK_GRAY, "include_tip": False, "stroke_width": 1, "include_numbers": False}, # Fainter, no numbers
tips=False
).scale(0.8).to_edge(LEFT, buff=LARGE_BUFF)
radius_val_unit = 1.0
origin_point_unit = axes_unit_faded.c2p(0, 0)
radius_point_unit = axes_unit_faded.c2p(radius_val_unit, 0)
screen_radius_unit = np.linalg.norm(radius_point_unit - origin_point_unit)
circle_unit_faded = Circle(radius=screen_radius_unit, color=MY_BLUE, stroke_width=1, arc_center=origin_point_unit) # Fainter
unit_circle_faded = VGroup(axes_unit_faded, circle_unit_faded)
# Cosine Graph (Right, Faded)
axes_graph_faded = Axes(
x_range=[0, 2 * PI + 0.1, PI / 2], y_range=[-1.2, 1.2, 1], x_length=8, y_length=4,
axis_config={"color": MY_DARK_GRAY, "include_tip": False, "stroke_width": 1, "include_numbers": False}, # Fainter, no numbers
tips=False
).to_edge(RIGHT, buff=LARGE_BUFF)
cosine_graph_faded = axes_graph_faded.plot(lambda x: np.cos(x), x_range=[0, 2 * PI], color=MY_ORANGE, stroke_width=1) # Fainter
graph_faded = VGroup(axes_graph_faded, cosine_graph_faded)
faded_elements = VGroup(unit_circle_faded, graph_faded).set_opacity(0.3)
# Final Text
final_text = Text("Cosine: The x-coordinate on the Unit Circle!",
font_size=48, color=MY_CONCLUSION_FG, weight=BOLD)
final_text.move_to(ORIGIN + UP * 0.5)
thanks_text = Text("Thank You!", font_size=36, color=MY_CONCLUSION_SUB)
thanks_text.next_to(final_text, DOWN, buff=MED_LARGE_BUFF)
# --- TTS ---
voice_text_05 = "So, remember: the cosine of an angle theta is simply the x-coordinate of the point where the terminal side of the angle intersects the unit circle. Understanding this connection is key to mastering trigonometry. Thank you for watching!"
with custom_voiceover_tts(voice_text_05) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path)
else:
print("Warning: Scene 5 TTS failed.")
subtitle_voice = Text(voice_text_05, font_size=28, color=MY_CONCLUSION_SUB, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
# --- Animation ---
self.play(FadeIn(faded_elements), run_time=1.0)
self.play(FadeIn(subtitle_voice), run_time=0.5)
self.play(FadeIn(final_text), run_time=2.0) # Use FadeIn for Text
self.play(FadeIn(thanks_text), run_time=1.0)
self.play(self.camera.frame.animate.scale(0.9).move_to(final_text.get_center()), run_time=1.5)
# Wait
anim_duration = 1.0 + 0.5 + 2.0 + 1.0 + 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(2)
# --- 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 for placeholder
# Create and render the scene
scene = CombinedScene()
scene.render()
print(f"Scene rendering finished. Output in: {config.media_dir}")```