什么是向量
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)
DEFAULT_FONT = "Noto Sans CJK SC" # Desired font for Chinese
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...")
fallback_fonts = ["PingFang SC", "Microsoft YaHei", "SimHei", "Arial Unicode MS"]
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 fallback fonts found. Using Manim default. Chinese characters may not display correctly.")
# final_font remains None
# --- Custom Colors ---
MY_DARK_BLUE = "#0d3b66"
MY_LIGHT_BLUE = "#faf0ca"
MY_GOLD = "#f4d35e"
MY_ORANGE = "#ee964b"
MY_RED = "#f95738"
MY_WHITE = "#FFFFFF"
MY_BLACK = "#000000"
MY_GRAY = "#888888"
# --- 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 Vectors
# -----------------------------
class CombinedScene(Scene):
"""
Explains the basic concept of vectors: magnitude and direction, with examples.
"""
def setup(self):
Scene.setup(self)
# Set default font if found
if final_font:
Text.set_default(font=final_font)
def construct(self):
# --- Scene Setup ---
# Background
bg = Rectangle(width=config.frame_width, height=config.frame_height,
fill_color=MY_DARK_BLUE, fill_opacity=1.0, stroke_width=0).set_z_index(-10)
self.add(bg)
# Scene Number
scene_num = Text("01", font_size=24, color=MY_LIGHT_BLUE).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(10)
self.add(scene_num)
# --- Introduction ---
title = Text("什么是向量?", font_size=60, color=MY_GOLD)
title.to_edge(UP, buff=MED_LARGE_BUFF)
definition_text = Text("向量是一个既有大小又有方向的量。", font_size=36, color=MY_LIGHT_BLUE)
definition_text.next_to(title, DOWN, buff=MED_LARGE_BUFF)
# Generic Vector Arrow
vector_arrow = Arrow(start=LEFT*2+DOWN*1, end=RIGHT*2+UP*1, color=MY_ORANGE, buff=0)
# Narration 1: Intro
voice_text_01 = "大家好!今天我们来了解一下什么是向量。向量是一个既有大小又有方向的量。"
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: Narration 1 TTS failed.")
subtitle_voice = Text(voice_text_01, font_size=28, color=MY_WHITE, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(
AnimationGroup(
FadeIn(title),
FadeIn(subtitle_voice, run_time=0.5),
lag_ratio=0.0
),
run_time=1.0
)
self.play(FadeIn(definition_text), run_time=1.5)
# Use Create for Arrow instead of GrowArrow if GrowArrow causes issues
self.play(Create(vector_arrow), run_time=1.5)
anim_duration = 1.0 + 1.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)
# --- Magnitude and Direction ---
magnitude_text = Text("大小 (Magnitude): 向量的长度 (模)", font_size=32, color=MY_LIGHT_BLUE)
direction_text = Text("方向 (Direction): 向量所指向的方向", font_size=32, color=MY_LIGHT_BLUE)
desc_group = VGroup(magnitude_text, direction_text).arrange(DOWN, aligned_edge=LEFT, buff=MED_SMALL_BUFF)
desc_group.next_to(vector_arrow, DOWN, buff=LARGE_BUFF)
# Highlight Magnitude (Brace)
brace_mag = Brace(vector_arrow, direction=vector_arrow.copy().rotate(PI/2).get_unit_vector(), color=MY_GOLD)
# FIX: Use Text for Chinese label, then position it using put_at_tip
brace_mag_label = Text("大小", font_size=30).set_color(MY_GOLD)
brace_mag.put_at_tip(brace_mag_label) # Position the Text label
# Highlight Direction (Focus on Arrowhead)
arrow_tip = vector_arrow.get_tip()
direction_highlight = Circle(radius=0.3, color=MY_RED, stroke_width=3).move_to(arrow_tip.get_center())
# Narration 2: Magnitude/Direction
voice_text_02 = "你可以把它想象成一个带箭头的线段。线段的长度代表向量的大小,也叫模。箭头的方向代表向量的方向。"
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: Narration 2 TTS failed.")
subtitle_voice = Text(voice_text_02, font_size=28, color=MY_WHITE, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(subtitle_voice), run_time=0.5)
# Animate the Text label along with the brace
self.play(FadeIn(desc_group[0]), GrowFromCenter(brace_mag), FadeIn(brace_mag_label), run_time=2.0)
self.play(FadeIn(desc_group[1]), Create(direction_highlight), run_time=2.0)
anim_duration = 0.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), FadeOut(brace_mag), FadeOut(brace_mag_label), FadeOut(direction_highlight), run_time=0.5)
# Fade out definition and arrow for examples
self.play(FadeOut(definition_text), FadeOut(vector_arrow), FadeOut(desc_group), run_time=1.0)
# --- Examples ---
examples_title = Text("例子 (Examples):", font_size=40, color=MY_GOLD)
examples_title.next_to(title, DOWN, buff=LARGE_BUFF).align_to(title, LEFT)
self.play(FadeIn(examples_title), run_time=1.0)
# Narration 3: Examples
voice_text_03 = "向量在物理学中有很多应用。例如,速度:像“向东 20 米每秒”就是一个向量,它既有大小(20米每秒),也有方向(东)。位移:比如“从 A 点到 B 点”,描述了方向和距离。还有力:例如“向右推力 5 牛顿”,指定了力的方向和大小。"
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: Narration 3 TTS failed.")
subtitle_voice = Text(voice_text_03, font_size=28, color=MY_WHITE, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(subtitle_voice), run_time=0.5)
# Example 1: Velocity
velocity_text = Text("速度: 例如,“向东 20 米/秒”", font_size=32, color=MY_LIGHT_BLUE)
velocity_arrow = Arrow(start=LEFT*2, end=RIGHT*2, color=MY_ORANGE, buff=0)
velocity_label = Text("20 m/s", font_size=24, color=MY_ORANGE).next_to(velocity_arrow, DOWN)
velocity_group = VGroup(velocity_text, velocity_arrow, velocity_label).arrange(DOWN, buff=MED_LARGE_BUFF)
velocity_group.next_to(examples_title, DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
self.play(FadeIn(velocity_group), run_time=1.5)
self.wait(3.0) # Wait for narration part
# Example 2: Displacement
displacement_text = Text("位移: 例如,“从 A 点到 B 点”", font_size=32, color=MY_LIGHT_BLUE)
point_a = Dot(LEFT*2+DOWN*1, color=MY_GOLD)
point_b = Dot(RIGHT*2+UP*1, color=MY_GOLD)
label_a = Text("A", font_size=24, color=MY_GOLD).next_to(point_a, DL)
label_b = Text("B", font_size=24, color=MY_GOLD).next_to(point_b, UR)
displacement_arrow = Arrow(point_a.get_center(), point_b.get_center(), color=MY_ORANGE, buff=0.1)
displacement_group = VGroup(displacement_text, point_a, point_b, label_a, label_b, displacement_arrow).arrange(DOWN, buff=MED_LARGE_BUFF)
displacement_group.next_to(examples_title, DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
self.play(Transform(velocity_group, displacement_group), run_time=1.5)
self.wait(3.0) # Wait for narration part
# Example 3: Force
force_text = Text("力: 例如,“向右推力 5 牛顿”", font_size=32, color=MY_LIGHT_BLUE)
box = Square(side_length=1.0, color=MY_LIGHT_BLUE, fill_opacity=0.5).move_to(ORIGIN)
force_arrow = Arrow(start=box.get_left()+LEFT*0.5, end=box.get_right()+RIGHT*0.5, color=MY_RED, buff=0)
force_label = Text("5 N", font_size=24, color=MY_RED).next_to(force_arrow, UP)
force_group = VGroup(force_text, box, force_arrow, force_label).arrange(DOWN, buff=MED_LARGE_BUFF)
force_group.next_to(examples_title, DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
self.play(Transform(displacement_group, force_group), run_time=1.5)
self.wait(3.0) # Wait for narration part
# Calculate wait time
anim_duration = 0.5 + 1.5 + 3.0 + 1.5 + 3.0 + 1.5 + 3.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)
# Fade out examples
self.play(FadeOut(examples_title), FadeOut(force_group), run_time=1.0)
# --- Mathematical Representation ---
math_title = Text("数学表示 (Mathematical Representation):", font_size=40, color=MY_GOLD)
math_title.next_to(title, DOWN, buff=LARGE_BUFF).align_to(title, LEFT)
math_desc1 = Text("在数学中,向量通常用坐标表示。", font_size=32, color=MY_LIGHT_BLUE)
math_desc1.next_to(math_title, DOWN, buff=MED_LARGE_BUFF, aligned_edge=LEFT)
# Coordinate System
axes = Axes(
x_range=[-1, 5, 1], y_range=[-1, 5, 1],
x_length=5, y_length=5,
axis_config={"color": MY_GRAY, "include_tip": True, "stroke_width": 2, "include_numbers": True},
tips=False
).add_coordinates()
axes.move_to(RIGHT*2 + DOWN*0.5)
# Vector (3, 4)
# FIX: Manually create the vector instead of using axes.get_vector
start_point = axes.c2p(0, 0)
end_point = axes.c2p(3, 4)
# Create the vector relative to the origin (0,0) first
# Use Vector instead of Arrow if just representing the vector itself
vec_3_4 = Vector(end_point - start_point, color=MY_ORANGE)
# Shift the vector so its start point is at the axes origin
vec_3_4.shift(start_point - vec_3_4.get_start())
vec_label = MathTex("(3, 4)", font_size=36, color=MY_ORANGE).next_to(vec_3_4.get_end(), UR, buff=SMALL_BUFF)
# Components
line_horz = DashedLine(axes.c2p(0, 4), axes.c2p(3, 4), color=MY_GRAY)
line_vert = DashedLine(axes.c2p(3, 0), axes.c2p(3, 4), color=MY_GRAY)
label_horz = MathTex("3", font_size=30, color=MY_GRAY).next_to(axes.c2p(1.5, 0), DOWN)
label_vert = MathTex("4", font_size=30, color=MY_GRAY).next_to(axes.c2p(0, 2), LEFT)
components = VGroup(line_horz, line_vert, label_horz, label_vert)
math_desc2 = Text("例如,向量 (3, 4) 表示一个从原点出发,\n水平方向移动 3 个单位,\n垂直方向移动 4 个单位的向量。",
font_size=32, color=MY_LIGHT_BLUE, line_spacing=1.2)
math_desc2.next_to(math_desc1, DOWN, buff=LARGE_BUFF, aligned_edge=LEFT)
math_desc2.shift(LEFT*4.5) # Position text to the left of axes
# Narration 4: Math Representation
voice_text_04 = "在数学中,我们经常用坐标来表示向量。比如在二维平面上,向量 (3, 4) 就表示一个从原点出发,先向右移动 3 个单位,再向上移动 4 个单位的向量。"
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: Narration 4 TTS failed.")
subtitle_voice = Text(voice_text_04, font_size=28, color=MY_WHITE, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeIn(math_title), FadeIn(math_desc1), run_time=1.5)
self.play(Create(axes), run_time=1.5)
self.play(FadeIn(subtitle_voice), run_time=0.5)
# FIX: Use Create for the vector animation
self.play(Create(vec_3_4), FadeIn(vec_label), run_time=1.5)
self.play(FadeIn(math_desc2), run_time=1.5)
self.play(Create(components), run_time=1.5)
anim_duration = 1.5 + 1.5 + 0.5 + 1.5 + 1.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)
# Fade out math section
# FIX: Ensure the manually created vec_3_4 is included in the group for FadeOut
math_group = VGroup(math_title, math_desc1, math_desc2, axes, vec_3_4, vec_label, components)
self.play(FadeOut(math_group), run_time=1.0)
# --- Conclusion ---
conclusion_text = Text("这就是向量的基本概念!", font_size=48, color=MY_GOLD)
conclusion_text.move_to(ORIGIN)
# Narration 5: Conclusion
voice_text_05 = "这就是向量的基本概念!希望这个解释对你有帮助。"
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: Narration 5 TTS failed.")
subtitle_voice = Text(voice_text_05, font_size=28, color=MY_WHITE, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=MED_SMALL_BUFF)
self.play(FadeOut(title), run_time=0.5) # Fade out original title
self.play(FadeIn(conclusion_text), FadeIn(subtitle_voice), run_time=1.5)
anim_duration = 0.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(2) # Final wait
# --- 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
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/499830182130651136/videos/1080p30/CombinedScene.mp4