PYPH 151 homework 1 Quesiton 2
Quesiton
- Explain why a vector cannot have a component greater than its own magnitude.
A vector’s magnitude is the Pythagorean sum of its components (i.e. (a = \sqrt{d_x^2 + d_y^2})). Squaring both sides gives (a^2 = d_x^2 + d_y^2). Since a square is always positive or zero, it must be that the magnitude squared ((a^2)) is itself always equal or greater than either of the components squared ((d_x^2) or (d_y^2)). This implies that any given component of a vector is never greater than the vector’s magnitude.
Sence Prompt
Okay, here's the explanation of why a vector's component cannot be greater than its own magnitude, along with a breakdown to make it clear:
The Basic Idea
A vector's component represents its "projection" onto a specific axis (like the x-axis or y-axis). The component is effectively the "shadow" the vector casts on that axis. A shadow cannot be longer than the object casting it!
Detailed Explanation
Vector Representation:
- Imagine a vector V in a 2D space (it works the same in 3D, just harder to visualize). We can represent it as having components along the x-axis (Vx) and the y-axis (Vy). These components are scalars (just numbers) that tell us how much the vector points in each direction.
Magnitude:
- The magnitude of the vector, denoted as |V|, is its length. It's calculated using the Pythagorean theorem:
- |V| = √(Vx² + Vy²)
- The magnitude of the vector, denoted as |V|, is its length. It's calculated using the Pythagorean theorem:
Components as Projections:
- Vx is the projection of V onto the x-axis.
- Vy is the projection of V onto the y-axis.
The Key Relationship: Trigonometry
Let's say the angle between vector V and the x-axis is θ (theta). Then:
- Vx = |V| * cos(θ)
- Vy = |V| * sin(θ)
The sine and cosine functions have a crucial property: their values are always between -1 and 1 (inclusive). That is, -1 ≤ cos(θ) ≤ 1 and -1 ≤ sin(θ) ≤ 1.
Why Components Can't Exceed Magnitude
Because sine and cosine are bounded between -1 and 1, multiplying the magnitude |V| by cos(θ) or sin(θ) can only reduce or reverse the direction of the magnitude. It can never increase the magnitude's value.
Therefore:
- |Vx| = | |V| * cos(θ) | ≤ |V| (the absolute value of Vx must be less than or equal to the absolute value of V)
- |Vy| = | |V| * sin(θ) | ≤ |V|
This means the absolute value of any component can be at most equal to the magnitude, but never greater.
The component can be negative, of course. A negative component indicates that the vector points in the opposite direction of the axis. However, the absolute value of the component, which represents its "length" along that axis, cannot exceed the vector's overall magnitude.
Analogy
Think of a ladder leaning against a wall. The ladder is the vector. The height the ladder reaches on the wall is one component, and the distance from the wall to the base of the ladder is the other component. The ladder itself is the hypotenuse of a right triangle. The hypotenuse (the magnitude of the vector/the length of the ladder) is always longer than either side (the components).
In summary:
The component of a vector represents the projection of that vector onto an axis. Because this projection is always "shorter" (or at most the same length) as the original vector, the magnitude of the component cannot be greater than the magnitude of the original vector. This is fundamentally because of the limitations of trigonometric functions (sine and cosine) and the geometry of right triangles.
Code
# -*- coding: utf-8 -*-
import os
import numpy as np
import requests
from contextlib import contextmanager
from manim import *
import hashlib
import manimpango # For font checking
from moviepy import AudioFileClip # Correct import for AudioFileClip
# --- Custom Colors ---
MY_DARK_BLUE = "#1E3A8A" # Dark Blue
MY_LIGHT_GRAY = "#F3F4F6" # Light Gray
MY_MEDIUM_GRAY = "#D1D5DB" # Medium Gray
MY_GOLD = "#F59E0B" # Gold
MY_ORANGE = "#F97316" # Orange
MY_RED = "#DC2626" # Red
MY_WHITE = "#FFFFFF" # White
MY_BLACK = "#000000" # Black
MY_YELLOW = "#FBBF24" # Yellow for vector
MY_GREEN = "#10B981" # Green for components
MY_PURPLE = "#8B5CF6" # Purple for angle/highlight
# --- Font Check ---
DEFAULT_FONT = "Noto Sans" # A common sans-serif font
available_fonts = manimpango.list_fonts()
final_font = None
if DEFAULT_FONT in available_fonts:
print(f"Font '{DEFAULT_FONT}' found.")
final_font = DEFAULT_FONT
else:
print(f"Warning: Font '{DEFAULT_FONT}' not found. Trying fallback fonts...")
# Common fallbacks, adjust as needed
fallback_fonts = ["Arial", "Helvetica", "Verdana", "Tahoma", "DejaVu Sans"]
found_fallback = False
for font in fallback_fonts:
if font in available_fonts:
print(f"Switched to fallback font: '{font}'")
final_font = font
found_fallback = True
break
if not found_fallback:
print(f"Warning: Neither '{DEFAULT_FONT}' nor any fallback fonts were found. Using Manim's default font.")
# final_font remains None
# --- TTS Caching Setup ---
CACHE_DIR = "tts_cache"
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."""
# Use a hash of the text for a unique filename
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.
Usage: with custom_voiceover_tts("text") as tracker: ...
"""
cache_file = get_cache_filename(text)
audio_file = cache_file # Initialize audio_file
if os.path.exists(cache_file):
# print(f"Using cached TTS for: {text[:30]}...")
audio_file = cache_file
else:
# print(f"Requesting TTS for: {text[:30]}...")
try:
# URL encode the input text to handle special characters
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) # Added timeout
response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
with open(cache_file, "wb") as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk: # filter out keep-alive new chunks
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}")
# Fallback: create a dummy tracker with zero duration
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return # Exit context manager
except Exception as e:
# Clean up potentially incomplete cache file on error
if os.path.exists(cache_file):
os.remove(cache_file)
print(f"An error occurred during TTS processing: {e}")
tracker = CustomVoiceoverTracker(None, 0)
yield tracker
return # Exit context manager
# Ensure audio file exists before processing with MoviePy
if audio_file and os.path.exists(audio_file):
try:
# Use context manager for AudioFileClip
with AudioFileClip(audio_file) as clip:
duration = clip.duration
# print(f"Audio duration: {duration:.2f}s")
tracker = CustomVoiceoverTracker(audio_file, duration)
except Exception as e:
print(f"Error processing audio file {audio_file}: {e}")
# Fallback if audio file is corrupted or invalid
# Clean up potentially corrupted cache file
if os.path.exists(cache_file):
try:
os.remove(cache_file)
print(f"Removed potentially corrupted cache file: {cache_file}")
except OSError as remove_error:
print(f"Error removing cache file {cache_file}: {remove_error}")
tracker = CustomVoiceoverTracker(None, 0)
else:
# Fallback if audio file was not created or found
print(f"TTS audio file not found or not created: {audio_file}")
tracker = CustomVoiceoverTracker(None, 0)
try:
yield tracker
finally:
# Decide whether to clean up cache here or keep it
# For now, we keep the cache unless an error occurred during processing
pass
# -----------------------------
# CombinedScene: Explains vector components vs. magnitude
# -----------------------------
class CombinedScene(MovingCameraScene):
"""
Explains why a vector's component cannot be greater than its magnitude.
"""
def setup(self):
"""Set default font if available."""
MovingCameraScene.setup(self)
if final_font:
Text.set_default(font=final_font)
print(f"Default font set to: {final_font}")
else:
print("Using Manim's default font.")
def construct(self):
# Use a scene-specific time tracker if needed for non-TTS animations
self.scene_time_tracker = ValueTracker(0)
# --- Play Scenes Sequentially ---
self.play_scene_01_intro()
self.clear_and_reset()
self.play_scene_02_components_magnitude()
self.clear_and_reset()
self.play_scene_03_trigonometry()
self.clear_and_reset()
self.play_scene_04_analogy_summary()
self.clear_and_reset()
# End of animation message
final_message = Text("Animation finished, thanks for watching! 😊", font_size=48, color=MY_WHITE)
bg_final = Rectangle(width=config.frame_width, height=config.frame_height, fill_color=MY_BLACK, fill_opacity=1,
stroke_width=0).set_z_index(-10)
self.add(bg_final)
self.play(FadeIn(final_message))
self.wait(2)
def get_scene_number(self, number_str):
"""Creates and positions the scene number."""
scene_num = Text(number_str, font_size=24, color=MY_WHITE)
scene_num.to_corner(UR, buff=0.3)
scene_num.set_z_index(10)
return scene_num
def clear_and_reset(self):
"""Clears all objects and resets the camera."""
# Clear updaters from all mobjects
mobjects_to_clear = list(self.mobjects) # Create a copy to iterate over
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 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)
# Clear the scene's mobject list
self.clear()
# Reset camera
self.camera.frame.move_to(ORIGIN)
self.camera.frame.set(width=config.frame_width, height=config.frame_height)
# Reset scene time tracker
self.scene_time_tracker.set_value(0)
self.wait(0.1)
# --- Scene 1: Introduction & Setup ---
def play_scene_01_intro(self):
"""Scene 1: Title, basic idea, vector setup."""
self.scene_time_tracker.set_value(0)
# Background
bg1 = Rectangle(
width=config.frame_width, height=config.frame_height,
fill_color=MY_BLACK, fill_opacity=1.0, stroke_width=0
).set_z_index(-10)
self.add(bg1)
# Scene Number
scene_num_01 = self.get_scene_number("01")
self.add(scene_num_01)
# Title
title = Text("Vector Components vs. Magnitude", font_size=48, color=MY_WHITE, weight=BOLD)
subtitle = Text("Why a component can't be larger than the vector itself", font_size=36, color=MY_LIGHT_GRAY)
title_group = VGroup(title, subtitle).arrange(DOWN, buff=0.3).to_edge(UP, buff=1.0)
# Basic Idea Text
idea_text = Text(
"Think of a component as the vector's 'shadow' on an axis.\nA shadow can't be longer than the object casting it!",
font_size=32, color=MY_WHITE, line_spacing=1.2, should_center=True,
width=config.frame_width - 4 # Allow wrapping
).shift(DOWN * 0.5)
# --- TTS Integration ---
voice_text_01 = "Hello! Today we'll explore a fundamental concept in vectors: why can a vector's component never be greater than its own magnitude? The basic idea is simple: think of a component as the vector's 'shadow' cast onto an axis. A shadow can't be longer than the object casting it!"
with custom_voiceover_tts(voice_text_01) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path, time_offset=0)
else:
print("Warning: Scene 1 TTS audio failed or has zero duration.")
subtitle_voice = Text(
voice_text_01, font_size=30, color=MY_WHITE,
width=config.frame_width - 2, should_center=True
).to_edge(DOWN, buff=0.5)
# Animations
self.play(
AnimationGroup(
FadeIn(subtitle_voice, run_time=0.5),
FadeIn(title_group, shift=DOWN * 0.5, run_time=1.5),
lag_ratio=0.0
),
run_time=1.5
)
self.wait(1.0)
self.play(FadeIn(idea_text, shift=UP * 0.3), run_time=2.0)
# Calculate wait time
anim_time = 1.5 + 1.0 + 2.0
if tracker.duration > 0:
remaining_time = tracker.duration - anim_time - 1.0 # Subtract fade out time
if remaining_time > 0:
self.wait(remaining_time)
else:
self.wait(1.5) # Wait if no audio
self.play(FadeOut(subtitle_voice), run_time=1.0)
self.wait(1)
# --- Scene 2: Components & Magnitude ---
def play_scene_02_components_magnitude(self):
"""Scene 2: Show vector, components, magnitude, Pythagorean theorem."""
self.scene_time_tracker.set_value(0)
# Background
bg2 = Rectangle(
width=config.frame_width, height=config.frame_height,
fill_color=MY_BLACK, fill_opacity=1.0, stroke_width=0
).set_z_index(-10)
self.add(bg2)
# Scene Number
scene_num_02 = self.get_scene_number("02")
self.add(scene_num_02)
# Axes
axes = Axes(
x_range=[-1, 7, 1], y_range=[-1, 5, 1],
x_length=8, y_length=5,
axis_config={"color": MY_WHITE, "include_tip": True, "stroke_width": 2},
x_axis_config={"include_numbers": True},
y_axis_config={"include_numbers": True},
tips=False # Tips handled in axis_config
).add_coordinates().shift(DOWN * 0.5) # Shift axes down slightly
axes_labels = axes.get_axis_labels(x_label="x", y_label="y")
axes_labels.set_color(MY_WHITE)
# Vector V
vec_end_coord = np.array([4, 3, 0]) # Example vector endpoint
vector_V = Vector(vec_end_coord, color=MY_YELLOW, stroke_width=6)
vector_label_V = MathTex(r"\vec{V}", color=MY_YELLOW, font_size=40)
vector_label_V.next_to(vector_V.get_center(), UR, buff=0.1) # Position label near middle/end
# Components (Projections) - Manual Dashed Lines
origin_point = axes.c2p(0, 0)
tip_point = axes.c2p(vec_end_coord[0], vec_end_coord[1])
x_proj_point = axes.c2p(vec_end_coord[0], 0)
y_proj_point = axes.c2p(0, vec_end_coord[1])
line_to_x = DashedLine(tip_point, x_proj_point, color=MY_GREEN, stroke_width=3, dash_length=0.15)
line_to_y = DashedLine(tip_point, y_proj_point, color=MY_GREEN, stroke_width=3, dash_length=0.15)
# Component Vectors (optional, can use lines on axes)
vector_Vx = Line(origin_point, x_proj_point, color=MY_GREEN, stroke_width=5)
vector_Vy = Line(origin_point, y_proj_point, color=MY_GREEN, stroke_width=5)
# Component Labels
label_Vx = MathTex("V_x", color=MY_GREEN, font_size=36).next_to(vector_Vx, DOWN, buff=0.2)
label_Vy = MathTex("V_y", color=MY_GREEN, font_size=36).next_to(vector_Vy, LEFT, buff=0.2)
# Magnitude Label
magnitude_label_V = MathTex(r"|\vec{V}|", color=MY_YELLOW, font_size=40)
# Position magnitude label along the vector
magnitude_label_V.move_to(vector_V.get_center() + vector_V.get_unit_vector() * 0.3 + vector_V.copy().rotate(PI/2).get_unit_vector() * 0.4)
# Pythagorean Theorem Formula
pythagorean_formula = MathTex(
r"|\vec{V}|^2 = V_x^2 + V_y^2",
font_size=48, color=MY_WHITE
).to_edge(UP, buff=1.0)
pythagorean_explanation = Text(
"(Magnitude squared equals sum of squares of components)",
font_size=28, color=MY_LIGHT_GRAY
).next_to(pythagorean_formula, DOWN, buff=0.2)
# Right Triangle visualization
right_angle_symbol = Square(side_length=0.2, color=MY_WHITE, stroke_width=2, fill_opacity=0)
right_angle_symbol.move_to(axes.c2p(0,0)).align_to(axes.c2p(0,0), DL) # Position at origin corner
# --- TTS Integration ---
voice_text_02 = "Let's visualize this. Here's a vector V in a 2D plane, starting from the origin. Its components, Vx and Vy, are its projections onto the x and y axes. You can see them forming a right-angled triangle with the vector V as the hypotenuse. The length of the vector, its magnitude |V|, is related to the components by the Pythagorean theorem: the square of the magnitude equals the sum of the squares of the components."
with custom_voiceover_tts(voice_text_02) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path, time_offset=0)
else:
print("Warning: Scene 2 TTS audio failed or has zero duration.")
subtitle_voice = Text(
voice_text_02, font_size=30, color=MY_WHITE,
width=config.frame_width - 2, should_center=True
).to_edge(DOWN, buff=0.5)
# Animations
self.play(
AnimationGroup(
FadeIn(subtitle_voice, run_time=0.5),
Create(axes, run_time=2.0),
FadeIn(axes_labels, run_time=1.0),
lag_ratio=0.0
),
run_time=2.0
)
self.play(
GrowArrow(vector_V), # Use GrowArrow for vector
Write(vector_label_V),
run_time=1.5
)
self.wait(0.5)
# Show components and labels
self.play(
Create(line_to_x),
Create(line_to_y),
Create(vector_Vx),
Create(vector_Vy),
Write(label_Vx),
Write(label_Vy),
run_time=2.5
)
self.play(Create(right_angle_symbol), run_time=0.5) # Show right angle
self.wait(0.5)
# Show magnitude label and formula
self.play(Write(magnitude_label_V), run_time=1.0)
self.play(
Write(pythagorean_formula),
FadeIn(pythagorean_explanation, shift=UP*0.2),
run_time=2.5
)
# Calculate wait time
anim_time = 2.0 + 1.5 + 0.5 + 2.5 + 0.5 + 0.5 + 1.0 + 2.5
if tracker.duration > 0:
remaining_time = tracker.duration - anim_time - 1.0 # Subtract fade out time
if remaining_time > 0:
self.wait(remaining_time)
else:
self.wait(1.5) # Wait if no audio
self.play(FadeOut(subtitle_voice), run_time=1.0)
self.wait(1)
# Store axes and vector for next scene if needed, or recreate
self.axes = axes
self.vector_V = vector_V
self.vec_end_coord = vec_end_coord
# --- Scene 3: Trigonometric Relationship ---
def play_scene_03_trigonometry(self):
"""Scene 3: Introduce angle, trig formulas, sin/cos bounds."""
self.scene_time_tracker.set_value(0)
# Background
bg3 = Rectangle(
width=config.frame_width, height=config.frame_height,
fill_color=MY_BLACK, fill_opacity=1.0, stroke_width=0
).set_z_index(-10)
self.add(bg3)
# Scene Number
scene_num_03 = self.get_scene_number("03")
self.add(scene_num_03)
# Recreate or reuse elements from Scene 2
# Recreating for clarity and independence
axes = Axes(
x_range=[-1, 7, 1], y_range=[-1, 5, 1],
x_length=8, y_length=5,
axis_config={"color": MY_WHITE, "include_tip": True, "stroke_width": 2},
x_axis_config={"include_numbers": True},
y_axis_config={"include_numbers": True},
tips=False
).add_coordinates().shift(DOWN * 0.5)
axes_labels = axes.get_axis_labels(x_label="x", y_label="y").set_color(MY_WHITE)
vec_end_coord = np.array([4, 3, 0])
vector_V = Vector(vec_end_coord, color=MY_YELLOW, stroke_width=6)
vector_label_V = MathTex(r"\vec{V}", color=MY_YELLOW, font_size=40).next_to(vector_V.get_center(), UR, buff=0.1)
origin_point = axes.c2p(0, 0)
tip_point = axes.c2p(vec_end_coord[0], vec_end_coord[1])
x_proj_point = axes.c2p(vec_end_coord[0], 0)
y_proj_point = axes.c2p(0, vec_end_coord[1])
vector_Vx = Line(origin_point, x_proj_point, color=MY_GREEN, stroke_width=5)
vector_Vy = Line(origin_point, y_proj_point, color=MY_GREEN, stroke_width=5)
label_Vx = MathTex("V_x", color=MY_GREEN, font_size=36).next_to(vector_Vx, DOWN, buff=0.2)
label_Vy = MathTex("V_y", color=MY_GREEN, font_size=36).next_to(vector_Vy, LEFT, buff=0.2)
magnitude_label_V = MathTex(r"|\vec{V}|", color=MY_YELLOW, font_size=40)
magnitude_label_V.move_to(vector_V.get_center() + vector_V.get_unit_vector() * 0.3 + vector_V.copy().rotate(PI/2).get_unit_vector() * 0.4)
# Add existing elements immediately
self.add(bg3, scene_num_03, axes, axes_labels, vector_V, vector_label_V, vector_Vx, vector_Vy, label_Vx, label_Vy, magnitude_label_V)
# Angle Theta
angle_theta = Angle(axes.x_axis, vector_V, radius=1.0, other_angle=False, color=MY_PURPLE)
label_theta = MathTex(r"\theta", color=MY_PURPLE, font_size=36)
label_theta.move_to(Angle(axes.x_axis, vector_V, radius=1.0 + 0.3).get_center()) # Position label outside arc
# Trigonometric Formulas
trig_formula_Vx = MathTex(r"V_x = |\vec{V}| \cos(\theta)", font_size=40, color=MY_WHITE)
trig_formula_Vy = MathTex(r"V_y = |\vec{V}| \sin(\theta)", font_size=40, color=MY_WHITE)
trig_formulas = VGroup(trig_formula_Vx, trig_formula_Vy).arrange(DOWN, buff=0.4).to_edge(RIGHT, buff=1.0).shift(UP*1.0)
# Sine/Cosine Bounds Explanation
bounds_text = Text("Key Property:", font_size=32, color=MY_WHITE, weight=BOLD)
bounds_formula = MathTex(r"-1 \le \cos(\theta) \le 1", r"\quad \text{and} \quad", r"-1 \le \sin(\theta) \le 1", font_size=40, color=MY_WHITE)
bounds_group = VGroup(bounds_text, bounds_formula).arrange(DOWN, buff=0.3).next_to(trig_formulas, DOWN, buff=0.8)
# Consequence Explanation
consequence_text = Text(
"Multiplying |V| by a number between -1 and 1\ncan only shrink it or keep it the same size.",
font_size=30, color=MY_LIGHT_GRAY, line_spacing=1.2, should_center=True
).next_to(bounds_group, DOWN, buff=0.5)
# Final Inequality
inequality_Vx = MathTex(r"|V_x| \le |\vec{V}|", font_size=40, color=MY_WHITE)
inequality_Vy = MathTex(r"|V_y| \le |\vec{V}|", font_size=40, color=MY_WHITE)
inequality_group = VGroup(inequality_Vx, inequality_Vy).arrange(DOWN, buff=0.4).next_to(consequence_text, DOWN, buff=0.6)
# --- TTS Integration ---
voice_text_03 = "Now, let's bring in trigonometry. If theta is the angle between the vector V and the positive x-axis, then the components can be expressed as: Vx equals magnitude V times cosine theta, and Vy equals magnitude V times sine theta. The crucial point here is that the values of sine and cosine are always between -1 and 1. Because of this, when you multiply the magnitude |V| by cosine theta or sine theta, the result's absolute value cannot be larger than |V| itself. Therefore, the absolute value of Vx must be less than or equal to the magnitude of V, and the same applies to Vy."
with custom_voiceover_tts(voice_text_03) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path, time_offset=0)
else:
print("Warning: Scene 3 TTS audio failed or has zero duration.")
subtitle_voice = Text(
voice_text_03, font_size=30, color=MY_WHITE,
width=config.frame_width - 2, should_center=True
).to_edge(DOWN, buff=0.5)
# Animations
self.play(FadeIn(subtitle_voice, run_time=0.5))
self.play(
Create(angle_theta),
Write(label_theta),
run_time=1.5
)
self.wait(0.5)
self.play(Write(trig_formulas), run_time=2.5)
self.wait(1.0)
# Highlight sin/cos bounds
highlight_bounds = SurroundingRectangle(bounds_formula, color=MY_PURPLE, buff=0.1)
self.play(
FadeIn(bounds_group, shift=UP*0.2),
run_time=2.0
)
# Use Succession for create-then-fade effect
self.play(
Succession(
Create(highlight_bounds, run_time=1.0),
FadeOut(highlight_bounds, run_time=1.0)
)
)
self.wait(0.5)
self.play(FadeIn(consequence_text), run_time=2.0)
self.wait(0.5)
self.play(Write(inequality_group), run_time=2.0)
# Calculate wait time
anim_time = 0.5 + 1.5 + 0.5 + 2.5 + 1.0 + 2.0 + 1.0 + 1.0 + 0.5 + 2.0 + 0.5 + 2.0 # Approx animation time
if tracker.duration > 0:
remaining_time = tracker.duration - anim_time - 1.0 # Subtract fade out time
if remaining_time > 0:
self.wait(remaining_time)
else:
self.wait(1.5) # Wait if no audio
self.play(FadeOut(subtitle_voice), run_time=1.0)
self.wait(1)
# --- Scene 4: Analogy & Summary ---
def play_scene_04_analogy_summary(self):
"""Scene 4: Ladder analogy and final summary."""
self.scene_time_tracker.set_value(0)
# Background
bg4 = Rectangle(
width=config.frame_width, height=config.frame_height,
fill_color=MY_DARK_BLUE, fill_opacity=1.0, stroke_width=0 # Dark blue background
).set_z_index(-10)
self.add(bg4)
# Scene Number
scene_num_04 = self.get_scene_number("04")
self.add(scene_num_04)
# Analogy Title
analogy_title = Text("Analogy: The Ladder 🪜", font_size=40, color=MY_WHITE, weight=BOLD)
analogy_title.to_edge(UP, buff=1.0).shift(LEFT * 3)
# Analogy Text
analogy_text = Text(
"Imagine a ladder (the vector) leaning against a wall.\n"
"• The height on the wall is one component.\n"
"• The distance from the wall is the other component.\n"
"• The ladder itself (magnitude) is always the longest part (hypotenuse).",
font_size=32, color=MY_LIGHT_GRAY, line_spacing=1.3,
width=config.frame_width / 2 - 1 # Fit on left side
).next_to(analogy_title, DOWN, buff=0.5, aligned_edge=LEFT)
# Simple Ladder Diagram (Optional, using lines)
wall = Line(LEFT * 1 + UP * 3, LEFT * 1 + DOWN * 1, color=MY_MEDIUM_GRAY, stroke_width=4)
ground = Line(LEFT * 1 + DOWN * 1, RIGHT * 3 + DOWN * 1, color=MY_MEDIUM_GRAY, stroke_width=4)
ladder = Line(LEFT * 1 + UP * 2.5, RIGHT * 2 + DOWN * 1, color=MY_ORANGE, stroke_width=6)
height_comp = DashedLine(LEFT * 1 + UP * 2.5, LEFT * 1 + DOWN * 1, color=MY_GREEN, dash_length=0.1)
base_comp = DashedLine(RIGHT * 2 + DOWN * 1, LEFT * 1 + DOWN * 1, color=MY_GREEN, dash_length=0.1)
ladder_diagram = VGroup(wall, ground, ladder, height_comp, base_comp)
ladder_diagram.scale(0.8).to_edge(RIGHT, buff=1.5).shift(UP*0.5)
# Summary Title
summary_title = Text("In Summary:", font_size=40, color=MY_WHITE, weight=BOLD)
summary_title.move_to(DOWN * 1.5).shift(LEFT * 3)
# Summary Points
summary_point1 = Text("• Components are projections (shadows).", font_size=32, color=MY_WHITE)
summary_point2 = Text("• Magnitude is the vector's full length.", font_size=32, color=MY_WHITE)
summary_point3 = Text("• Geometry & Trigonometry (sin/cos ≤ 1) limit component size.", font_size=32, color=MY_WHITE)
summary_group = VGroup(summary_point1, summary_point2, summary_point3).arrange(DOWN, buff=0.3, aligned_edge=LEFT)
summary_group.next_to(summary_title, DOWN, buff=0.4, aligned_edge=LEFT)
# --- TTS Integration ---
voice_text_04 = "Think of this analogy: a ladder leaning against a wall. The ladder is the vector. The height it reaches on the wall and its distance from the base of the wall are its components. The ladder itself, the hypotenuse, is always longer than either of those sides. So, in summary: vector components are like projections or shadows. The magnitude is the vector's total length. Basic geometry and the fact that sine and cosine never exceed 1 ensure that a component's absolute value cannot be greater than the vector's magnitude."
with custom_voiceover_tts(voice_text_04) as tracker:
if tracker.audio_path and tracker.duration > 0:
self.add_sound(tracker.audio_path, time_offset=0)
else:
print("Warning: Scene 4 TTS audio failed or has zero duration.")
subtitle_voice = Text(
voice_text_04, font_size=30, color=MY_WHITE,
width=config.frame_width - 2, should_center=True
).to_edge(DOWN, buff=0.5)
# Animations
self.play(
AnimationGroup(
FadeIn(subtitle_voice, run_time=0.5),
FadeIn(analogy_title, shift=RIGHT*0.2, run_time=1.5),
FadeIn(analogy_text, lag_ratio=0.1, run_time=2.0),
Create(ladder_diagram, lag_ratio=0.2, run_time=2.5),
lag_ratio=0.0
),
run_time=3.0 # Duration of the longest animation in the group
)
self.wait(1.0)
self.play(
FadeIn(summary_title, shift=UP*0.2),
FadeIn(summary_group, lag_ratio=0.2),
run_time=2.5
)
# Calculate wait time
anim_time = 3.0 + 1.0 + 2.5
if tracker.duration > 0:
remaining_time = tracker.duration - anim_time - 1.0 # Subtract fade out time
if remaining_time > 0:
self.wait(remaining_time)
else:
self.wait(2.0) # Wait if no audio
self.play(FadeOut(subtitle_voice), run_time=1.0)
self.wait(2) # Hold summary screen
# --- Main execution block ---
if __name__ == "__main__":
# Basic configuration
config.pixel_height = 1080 # Set resolution height
config.pixel_width = 1920 # Set resolution width
config.frame_rate = 30 # Set frame rate
config.output_file = "CombinedScene" # Specify output filename
config.disable_caching = True # Disable caching
# Set output directory using placeholder for Java replacement
config.media_dir = r"#(output_path)" # IMPORTANT: Use the placeholder
# 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/499178015963123712/videos/1080p30/CombinedScene.mp4