Tio Boot DocsTio Boot Docs
Home
  • java-db
  • api-table
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
Home
  • java-db
  • api-table
  • Enjoy
  • Tio Boot Admin
  • ai_agent
  • translator
  • knowlege_base
  • ai-search
  • 案例
Abount
  • Github
  • Gitee
  • 01_tio-boot 简介

    • tio-boot:新一代高性能 Java Web 开发框架
    • tio-boot 入门示例
    • Tio-Boot 配置 : 现代化的配置方案
    • tio-boot 整合 Logback
    • tio-boot 整合 hotswap-classloader 实现热加载
    • 自行编译 tio-boot
    • 最新版本
    • 开发规范
  • 02_部署

    • 使用 Maven Profile 实现分环境打包 tio-boot 项目
    • Maven 项目配置详解:依赖与 Profiles 配置
    • tio-boot 打包成 FastJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 胖包与瘦包的打包与部署
    • 使用 Jenkins 部署 Tio-Boot 项目
    • 使用 Nginx 反向代理 Tio-Boot
    • 使用 Supervisor 管理 Java 应用
  • 03_配置

    • 配置参数
    • 服务器监听器
    • 内置缓存系统 AbsCache
    • 使用 Redis 作为内部 Cache
    • 静态文件处理器
    • 基于域名的静态资源隔离
    • DecodeExceptionHandler
  • 04_原理

    • 生命周期
    • 请求处理流程
    • 重要的类
  • 05_json

    • Json
    • 接受 JSON 和响应 JSON
    • 响应实体类
  • 06_web

    • 概述
    • 文件上传
    • 接收请求参数
    • 接收日期参数
    • 接收数组参数
    • 返回字符串
    • 返回文本数据
    • 返回网页
    • 请求和响应字节
    • 文件下载
    • 返回视频文件并支持断点续传
    • http Session
    • Cookie
    • HttpRequest
    • HttpResponse
    • Resps
    • RespBodyVo
    • /zh/06_web/19.html
    • 全局异常处理器
    • 异步
    • 动态 返回 CSS 实现
    • 返回图片
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • 接口访问统计
    • 接口请求和响应数据记录
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 跨域
    • 添加 Controller
    • 常用工具类
    • HTTP Basic 认证
    • WebJars
    • JProtobuf
  • 07_validate

    • 数据紧校验规范
    • 参数校验
  • 08_websocket

    • 使用 tio-boot 搭建 WebSocket 服务
    • WebSocket 聊天室项目示例
  • 09_java-db

    • java‑db
    • 操作数据库入门示例
    • SQL 模板
    • 数据源配置与使用
    • ActiveRecord
    • Model
    • 生成器与 Model
    • Db 工具类
    • 批量操作
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • Java-DB 整合 Enjoy 模板最佳实践
    • 多数据源支持
    • 独立使用 ActiveRecord
    • 调用存储过程
    • java-db 整合 Guava 的 Striped 锁优化
    • 生成 SQL
    • 通过实体类操作数据库
    • java-db 读写分离
    • Spring Boot 整合 Java-DB
    • like 查询
    • 常用操作示例
    • Druid 监控集成指南
    • SQL 统计
  • 10_api-table

    • ApiTable 概述
    • 使用 ApiTable 连接 SQLite
    • 使用 ApiTable 连接 Mysql
    • 使用 ApiTable 连接 Postgres
    • 使用 ApiTable 连接 TDEngine
    • 使用 api-table 连接 oracle
    • 使用 api-table 连接 mysql and tdengine 多数据源
    • EasyExcel 导出
    • EasyExcel 导入
    • TQL(Table SQL)前端输入规范
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
  • 11_aop

    • JFinal-aop
    • Aop 工具类
    • 配置
    • 配置
    • 独立使用 JFinal Aop
    • @AImport
    • 原理解析
  • 12_cache

    • Caffine
    • Jedis-redis
    • hutool RedisDS
    • Redisson
    • Caffeine and redis
    • CacheUtils 工具类
    • 使用 CacheUtils 整合 caffeine 和 redis 实现的两级缓存
    • 使用 java-db 整合 ehcache
    • 使用 java-db 整合 redis
    • Java DB Redis 相关 Api
    • redis 使用示例
  • 13_认证和权限

    • hutool-JWT
    • FixedTokenInterceptor
    • 使用内置 TokenManager 实现登录
    • 用户系统
    • 重置密码
    • 匿名登录
    • Google 登录
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
    • 短信登录
    • 移动端微信登录实现指南
    • 移动端重置密码
  • 14_i18n

    • i18n
  • 15_enjoy

    • tio-boot 整合 Enjoy 模版引擎文档
    • 引擎配置
    • 表达式
    • 指令
    • 注释
    • 原样输出
    • Shared Method 扩展
    • Shared Object 扩展
    • Extension Method 扩展
    • Spring boot 整合
    • 独立使用 Enjoy
    • tio-boot enjoy 自定义指令 localeDate
    • PromptEngine
    • Enjoy 入门示例-擎渲染大模型请求体
    • Enjoy 使用示例
  • 16_定时任务

    • Quartz 定时任务集成指南
    • 分布式定时任务 xxl-jb
    • cron4j 使用指南
  • 17_tests

    • TioBootTest 类
  • 18_tio

    • TioBootServer
    • tio-core
    • 内置 TCP 处理器
    • 独立启动 UDPServer
    • 使用内置 UDPServer
    • t-io 消息处理流程
    • tio-运行原理详解
    • TioConfig
    • ChannelContext
    • Tio 工具类
    • 业务数据绑定
    • 业务数据解绑
    • 发送数据
    • 关闭连接
    • Packet
    • 监控: 心跳
    • 监控: 客户端的流量数据
    • 监控: 单条 TCP 连接的流量数据
    • 监控: 端口的流量数据
    • 单条通道统计: ChannelStat
    • 所有通道统计: GroupStat
    • 资源共享
    • 成员排序
    • ssl
    • DecodeRunnable
    • 使用 AsynchronousSocketChannel 响应数据
    • 拉黑 IP
    • 深入解析 Tio 源码:构建高性能 Java 网络应用
  • 19_aio

    • ByteBuffer
    • AIO HTTP 服务器
    • 自定义和线程池和池化 ByteBuffer
    • AioHttpServer 应用示例 IP 属地查询
    • 手写 AIO Http 服务器
  • 20_netty

    • Netty TCP Server
    • Netty Web Socket Server
    • 使用 protoc 生成 Java 包文件
    • Netty WebSocket Server 二进制数据传输
    • Netty 组件详解
  • 21_netty-boot

    • Netty-Boot
    • 原理解析
    • 整合 Hot Reload
    • 整合 数据库
    • 整合 Redis
    • 整合 Elasticsearch
    • 整合 Dubbo
    • Listener
    • 文件上传
    • 拦截器
    • Spring Boot 整合 Netty-Boot
    • SSL 配置指南
    • ChannelInitializer
    • Reserve
  • 22_MQ

    • Mica-mqtt
    • EMQX
    • Disruptor
  • 23_tio-utils

    • tio-utils
    • HttpUtils
    • Notification
    • 邮箱
    • JSON
    • 读取文件
    • Base64
    • 上传和下载
    • Http
    • Telegram
    • RsaUtils
    • EnvUtils 使用文档
    • 系统监控
    • 毫秒并发 ID (MCID) 生成方案
  • 24_tio-http-server

    • 使用 Tio-Http-Server 搭建简单的 HTTP 服务
    • tio-boot 添加 HttpRequestHandler
    • 在 Android 上使用 tio-boot 运行 HTTP 服务
    • tio-http-server-native
    • handler 常用操作
  • 25_tio-websocket

    • WebSocket 服务器
    • WebSocket Client
  • 26_tio-im

    • 通讯协议文档
    • ChatPacket.proto 文档
    • java protobuf
    • 数据表设计
    • 创建工程
    • 登录
    • 历史消息
    • 发消息
  • 27_mybatis

    • Tio-Boot 整合 MyBatis
    • 使用配置类方式整合 MyBatis
    • 整合数据源
    • 使用 mybatis-plus 整合 tdengine
    • 整合 mybatis-plus
  • 28_mongodb

    • tio-boot 使用 mongo-java-driver 操作 mongodb
  • 29_elastic-search

    • Elasticsearch
    • JavaDB 整合 ElasticSearch
    • Elastic 工具类使用指南
    • Elastic-search 注意事项
    • ES 课程示例文档
  • 30_magic-script

    • tio-boot 整合 magic-script
  • 31_groovy

    • tio-boot 整合 Groovy
  • 32_firebase

    • 整合 google firebase
    • Firebase Storage
    • Firebase Authentication
    • 使用 Firebase Admin SDK 进行匿名用户管理与自定义状态标记
    • 导出用户
    • 注册回调
    • 登录注册
  • 33_文件存储

    • 文件上传数据表
    • 本地存储
    • 使用 AWS S3 存储文件并整合到 Tio-Boot 项目中
    • 存储文件到 腾讯 COS
  • 34_spider

    • jsoup
    • 爬取 z-lib.io 数据
    • 整合 WebMagic
    • WebMagic 示例:爬取学校课程数据
    • Playwright
    • Flexmark (Markdown 处理器)
    • tio-boot 整合 Playwright
    • 缓存网页数据
  • 36_integration_thirty_party

    • tio-boot 整合 okhttp
    • 整合 GrpahQL
    • 集成 Mailjet
    • 整合 ip2region
    • 整合 GeoLite 离线库
    • 整合 Lark 机器人指南
    • 集成 Lark Mail 实现邮件发送
    • Thymeleaf
    • Swagger
    • Clerk 验证
  • 37_dubbo

    • 概述
    • dubbo 2.6.0
    • dubbo 2.6.0 调用过程
    • dubbo 3.2.0
  • 38_spring

    • Spring Boot Web 整合 Tio Boot
    • spring-boot-starter-webflux 整合 tio-boot
    • Tio Boot 整合 Spring Boot Starter
    • Tio Boot 整合 Spring Boot Starter Data Redis 指南
  • 39_spring-cloud

    • tio-boot spring-cloud
  • 40_mysql

    • 使用 Docker 运行 MySQL
    • /zh/42_mysql/02.html
  • 41_postgresql

    • PostgreSQL 安装
    • PostgreSQL 主键自增
    • PostgreSQL 日期类型
    • Postgresql 金融类型
    • PostgreSQL 数组类型
    • PostgreSQL 全文检索
    • PostgreSQL 查询优化
    • 获取字段类型
    • PostgreSQL 向量
    • PostgreSQL 优化向量查询
    • PostgreSQL 其他
  • 43_oceanbase

    • 快速体验 OceanBase 社区版
    • 快速上手 OceanBase 数据库单机部署与管理
    • 诊断集群性能
    • 优化 SQL 性能指南
    • /zh/43_oceanbase/05.html
  • 50_media

    • JAVE 提取视频中的声音
    • Jave 提取视频中的图片
    • /zh/50_media/03.html
  • 51_asr

    • Whisper-JNI
  • 54_native-media

    • java-native-media
    • JNI 入门示例
    • mp3 拆分
    • mp4 转 mp3
    • 使用 libmp3lame 实现高质量 MP3 编码
    • Linux 编译
    • macOS 编译
    • 从 JAR 包中加载本地库文件
    • 支持的音频和视频格式
    • 任意格式转为 mp3
    • 通用格式转换
    • 通用格式拆分
    • 视频合并
    • VideoToHLS
    • split_video_to_hls 支持其他语言
    • 持久化 HLS 会话
  • 55_telegram4j

    • 数据库设计
    • /zh/55_telegram4j/02.html
    • 基于 MTProto 协议开发 Telegram 翻译机器人
    • 过滤旧消息
    • 保存机器人消息
    • 定时推送
    • 增加命令菜单
    • 使用 telegram-Client
    • 使用自定义 StoreLayout
    • 延迟测试
    • Reactor 错误处理
    • Telegram4J 常见错误处理指南
  • 56_telegram-bots

    • TelegramBots 入门指南
    • 使用工具库 telegram-bot-base 开发翻译机器人
  • 60_LLM

    • 简介
    • AI 问答
    • /zh/60_LLM/03.html
    • /zh/60_LLM/04.html
    • 增强检索(RAG)
    • 结构化数据检索
    • 搜索+AI
    • 集成第三方 API
    • 后置处理
    • 推荐问题生成
    • 连接代码执行器
    • 避免 GPT 混乱
    • /zh/60_LLM/13.html
  • 61_ai_agent

    • 数据库设计
    • 示例问题管理
    • 会话管理
    • 历史记录
    • 对接 Perplexity API
    • 意图识别与生成提示词
    • 智能问答模块设计与实现
    • 文件上传与解析文档
    • 翻译
    • 名人搜索功能实现
    • Ai studio gemini youbue 问答使用说明
    • 自建 YouTube 字幕问答系统
    • 自建 获取 youtube 字幕服务
    • 通用搜索
    • /zh/61_ai_agent/15.html
    • 16
    • 17
    • 18
    • 在 tio-boot 应用中整合 ai-agent
    • 16
  • 62_translator

    • 简介
  • 63_knowlege_base

    • 数据库设计
    • 用户登录实现
    • 模型管理
    • 知识库管理
    • 文档拆分
    • 片段向量
    • 命中测试
    • 文档管理
    • 片段管理
    • 问题管理
    • 应用管理
    • 向量检索
    • 推理问答
    • 问答模块
    • 统计分析
    • 用户管理
    • api 管理
    • 存储文件到 S3
    • 文档解析优化
    • 片段汇总
    • 段落分块与检索
    • 多文档解析
    • 对话日志
    • 检索性能优化
    • Milvus
    • 文档解析方案和费用对比
    • 离线运行向量模型
  • 64_ai-search

    • ai-search 项目简介
    • ai-search 数据库文档
    • ai-search SearxNG 搜索引擎
    • ai-search Jina Reader API
    • ai-search Jina Search API
    • ai-search 搜索、重排与读取内容
    • ai-search PDF 文件处理
    • ai-search 推理问答
    • Google Custom Search JSON API
    • ai-search 意图识别
    • ai-search 问题重写
    • ai-search 系统 API 接口 WebSocket 版本
    • ai-search 搜索代码实现 WebSocket 版本
    • ai-search 生成建议问
    • ai-search 生成问题标题
    • ai-search 历史记录
    • Discover API
    • 翻译
    • Tavily Search API 文档
    • 对接 Tavily Search
    • 火山引擎 DeepSeek
    • 对接 火山引擎 DeepSeek
    • ai-search 搜索代码实现 SSE 版本
    • jar 包部署
    • Docker 部署
    • 爬取一个静态网站的所有数据
    • 网页数据预处理
    • 网页数据检索与问答流程整合
  • 65_java-linux

    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • MCP 协议
    • Cline 提示词
    • Cline 提示词-中文版本
  • 66_manim

    • 简介
    • Manim 开发环境搭建
    • 生成场景提示词
    • 生成代码
    • 完整脚本示例
    • 语音合成系统
    • Fish.audio TTS 接口说明文档与 Java 客户端封装
    • 整合 fishaudio 到 java-uni-ai-server 项目
    • 执行 Python (Manim) 代码
    • 使用 SSE 流式传输生成进度的实现文档
    • 整合全流程完整文档
    • HLS 动态推流技术文档
    • manim 分场景生成代码
    • 分场景运行代码及流式播放支持
    • 分场景业务端完整实现流程
    • Maiim布局管理器
    • 仅仅生成场景代码
    • 使用 modal 运行 manim 代码
    • Python 使用 Modal GPU 加速渲染
    • Modal 平台 GPU 环境下运行 Manim
    • Modal Manim OpenGL 安装与使用
    • 优化 GPU 加速
    • 生成视频封面流程
    • Java 调用 manim 命令 执行代码 生成封面
    • Manim 图像生成服务客户端文档
    • manim render help
    • 显示 中文公式
    • manimgl
    • EGL
    • /zh/66_manim/30.html
    • /zh/66_manim/31.html
    • 成本核算
    • /zh/66_manim/33.html
  • 70_tio-boot-admin

    • 入门指南
    • 初始化数据
    • token 存储
    • 与前端集成
    • 文件上传
    • 网络请求
    • 图片管理
    • /zh/70_tio-boot-admin/08.html
    • Word 管理
    • PDF 管理
    • 文章管理
    • 富文本编辑器
  • 71_tio-boot

    • /zh/71_tio-boot/01.html
    • Swagger 整合到 Tio-Boot 中的指南
    • HTTP/1.1 Pipelining 性能测试报告
  • 80_性能测试

    • 压力测试 - tio-http-serer
    • 压力测试 - tio-boot
    • 压力测试 - tio-boot-native
    • 压力测试 - netty-boot
    • 性能测试对比
    • TechEmpower FrameworkBenchmarks
    • 压力测试 - tio-boot 12 C 32G
  • 99_案例

    • 封装 IP 查询服务
    • tio-boot 案例 - 全局异常捕获与企业微信群通知
    • tio-boot 案例 - 文件上传和下载
    • tio-boot 案例 - 整合 ant design pro 增删改查
    • tio-boot 案例 - 流失响应
    • tio-boot 案例 - 增强检索
    • tio-boot 案例 - 整合 function call
    • tio-boot 案例 - 定时任务 监控 PostgreSQL、Redis 和 Elasticsearch
    • Tio-Boot 案例:使用 SQLite 整合到登录注册系统
    • tio-boot 案例 - 执行 shell 命令

#《Manim 实战入门:用代码创造数学动画》

Tong Li

前言

欢迎来到 Manim 的世界!Manim 是一个强大的动画引擎,最初由 Grant Sanderson (3Blue1Brown) 开发,用于制作精美的数学解释视频。它使用 Python 编程语言,让您能够通过代码精确控制动画的每一个细节,创造出专业级的可视化效果。

这本书是为初学者设计的,特别是那些希望通过实际案例学习 Manim 的朋友们。我们将直接从您提供的这些优秀 Python 脚本入手,逐个剖析其中的知识点。每个概念都会结合具体的代码进行讲解,让您不仅知道“是什么”,更明白“怎么做”以及“为什么这么做”。

无论您是学生、教师、科普爱好者,还是仅仅对用代码创作动画感兴趣,这本书都将为您打开一扇新的大门。

本书结构

我们将从最基础的 Manim 场景搭建开始,逐步深入到更复杂的动画技巧、图形绘制、布局管理和自定义配置。每一章都会围绕一个或多个核心知识点,并从您提供的脚本中选取最相关的代码片段进行分析。

准备工作

在开始之前,请确保您已经:

  1. 安装了 Python 环境。
  2. 按照 Manim 官方文档的指引,成功安装了 Manim 及其依赖项(如 LaTeX, FFmpeg等)。
  3. 准备一个您喜欢的代码编辑器(如 VS Code, PyCharm 等)。

让我们开始这段奇妙的 Manim 之旅吧!


第一章:Manim 初探——搭建你的第一个场景

Manim 的核心是 场景 (Scene)。一个场景就是一段独立的动画。每个 Manim 脚本通常包含一个或多个继承自 manim.Scene 的类。

1.1 基本场景结构

让我们看一个最简单的场景结构,这在您提供的几乎所有脚本中都有体现:

from manim import * # 导入 Manim 库的所有内容

class MyFirstScene(Scene): # 定义一个场景类,继承自 Scene
    def construct(self): # construct 方法是场景的入口点
        # 在这里编写动画逻辑
        text = Text("你好,Manim!") # 创建一个文本对象
        self.play(Write(text)) # 播放“书写”文本的动画
        self.wait(1) # 等待1秒
  • from manim import *: 这行代码导入了 Manim 库中所有可用的类、函数和常量。对于初学者来说,这很方便。
  • class MyFirstScene(Scene):: 我们定义了一个名为 MyFirstScene 的类,它继承了 Manim 的 Scene 基类。这意味着 MyFirstScene 将拥有 Scene 类的所有功能。
  • def construct(self):: 这是每个场景类中最重要的方法。Manim 会自动调用这个方法来构建和播放场景中的动画。self 代表场景对象本身。
  • text = Text("你好,Manim!"): Text 是 Manim 中用于创建文本的类 (Mobject - Manim Object)。这里我们创建了一个内容为“你好,Manim!”的文本对象。
  • self.play(Write(text)): self.play() 是播放动画的指令。Write(text) 是一种动画效果,它会模拟笔迹书写出 text 对象。
  • self.wait(1): 动画播放完毕后,让场景暂停1秒。

1.2 运行 Manim 脚本

要运行上面的脚本(假设保存为 my_scene.py),你可以在终端中使用以下命令:

manim -pql my_scene.py MyFirstScene
  • manim: 调用 Manim 命令行工具。
  • -pql: 选项的组合。
    • p: 表示预览 (preview),即播放动画。
    • q: 表示质量 (quality)。
    • l: 表示低质量 (low quality),渲染速度快,适合调试。其他选项有 m (medium), h (high), k (4k)。
  • my_scene.py: 你的 Python 文件名。
  • MyFirstScene: 你想渲染的场景类名。

1.3 场景背景色和默认颜色

在您提供的多个脚本中,我们看到了设置场景背景色和Mobject默认颜色的方法:

  • 25_Parabola_equation_function_graph_changes.txt
  • 26_trigonometric_function.txt
  • 27_find the equation of the line.txt
  • 28_find_the_value_of_function.txt
  • 29_find_angles_of_triangles.txt
  • 30_binary_tree.txt
  • 31_rotational_kinetic_energy_and_angular_momentum.txt

这些脚本中,通常在 construct 方法的开头或 __init__ (或 setup) 方法中设置:

# 摘自 25_Parabola_equation_function_graph_changes.txt (稍作修改以便说明)
class CombinedScene(Scene):
    def setup(self): # setup 方法在 construct 之前被调用
        Scene.setup(self) # 调用父类的 setup
        Mobject.set_default(color=BLACK) # 设置所有 Mobject 的默认颜色为黑色
        self.camera.background_color = WHITE # 设置场景背景色为白色
        Text.set_default(font_size=28, color=BLACK)
        Title.set_default(font_size=48, color=BLACK) # Title 是一个自定义或特定用途的类
        MathTex.set_default(font_size=40, color=BLACK)

    def construct(self):
        # self.camera.background_color = WHITE # 也可以在这里设置
        # ... 其他代码 ...
        pass
  • self.camera.background_color = WHITE: self.camera 是场景的相机对象,通过修改其 background_color 属性可以改变背景颜色。WHITE 是 Manim 预定义的颜色常量。
  • Mobject.set_default(color=BLACK): Mobject 是 Manim 中所有图形对象的基类。这行代码将所有后续创建的 Mobject 的默认颜色设置为黑色。
  • Text.set_default(...) 和 MathTex.set_default(...): 类似地,可以为特定类型的 Mobject (如 Text, MathTex) 设置默认属性。

1.4 字体设置 (特别是中文字体)

Manim 默认的 LaTeX 环境可能不直接支持中文。在处理中文文本时,需要进行特殊配置。您提供的脚本 31_rotational_kinetic_energy_and_angular_momentum.txt 和 26_trigonometric_function.txt 展示了如何配置中文字体,特别是针对 MathTex(使用 LaTeX 渲染数学公式和文本)。

# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt

# ... (导入 os, sys)
# from manim_utils import ..., get_available_font, ... # 假设的工具函数

CJK_FONT_NAME = "Songti SC" # 定义中文字体名称 (宋体-简)
cjk_template = TexTemplate(
    tex_compiler="xelatex", # 使用 xelatex 编译器,对 Unicode 支持更好
    output_format=".xdv",
    preamble=rf"""
\usepackage{{amsmath}}
\usepackage{{amssymb}}
\usepackage{{fontspec}}
\usepackage{{xeCJK}}
\setCJKmainfont{{{CJK_FONT_NAME}}}
"""
)

class CombinedScene(Scene):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        # ...
        final_font = get_available_font() # 假设这个函数能获取一个可用的系统字体
        if final_font:
            Text.set_default(font=final_font) # 设置 Text 对象的默认字体

        MathTex.set_default(tex_template=cjk_template) # 为 MathTex 设置自定义的 TeX 模板
        Text.set_default(color=self.text_color) # self.text_color 可能是 BLACK
        MathTex.set_default(color=self.text_color)
    # ...
  • CJK_FONT_NAME: 指定了要在 LaTeX 中使用的中文字体名称。你需要确保你的系统上安装了这个字体,并且 LaTeX 能够找到它。常见的选择有 "SimSun", "Microsoft YaHei", "Songti SC", "PingFang SC" 等。
  • TexTemplate: Manim 允许你自定义用于渲染 MathTex 和 Tex 对象的 LaTeX 模板。
    • tex_compiler="xelatex": xelatex 是一个对 Unicode 和现代字体格式 (如 OpenType) 支持非常好的 LaTeX 编译器,推荐用于处理中文。
    • preamble: LaTeX 文档的导言区。
      • \usepackage{fontspec}: 允许使用系统安装的字体。
      • \usepackage{xeCJK}: 提供了在 xelatex 中处理中日韩文字的宏包。
      • \setCJKmainfont{{{CJK_FONT_NAME}}}: 将指定的中文字体设置为主 CJK 字体。
  • MathTex.set_default(tex_template=cjk_template): 将这个自定义的 cjk_template 应用于所有 MathTex 对象。
  • Text.set_default(font=final_font): 对于普通的 Text 对象(不通过 LaTeX 渲染),可以直接指定字体名称。get_available_font() 是一个自定义函数,用于获取一个系统中可用的字体名称。如果你的系统中有中文字体,并且 Manim 能找到它,Text 对象也能显示中文。

1.5 辅助工具:场景编号和布局管理

您的许多脚本都使用了一个名为 update_scene_number 的方法和一个 Layout 类。这些通常是用户自定义的辅助工具,用于更好地组织和管理复杂的动画项目。

  • update_scene_number:

    # 类似这样的实现,在多个脚本中出现
    # self.current_scene_num_mob = None # 通常在 __init__ 或 setup 中初始化
    
    def update_scene_number(self, number_str):
        new_scene_num = Text(number_str, font_size=24, color=self.text_color).to_corner(UR, buff=MED_LARGE_BUFF).set_z_index(100)
        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
    

    这个方法的作用是在屏幕右上角显示当前的场景(或小节)编号,并在更新时带有淡入淡出效果。这对于制作系列视频或长视频时标记进度非常有用。

    • Text(...).to_corner(UR, ...): 创建文本并将其定位到右上角 (Upper Right)。
    • .set_z_index(100): 确保编号显示在其他元素的上方。
    • FadeIn 和 FadeOut: 淡入淡出动画。
  • Layout 类:

    # 伪代码,基于脚本中的使用推断
    # from manim_utils import Layout, LayoutAtom, LayoutDirection
    
    # layout = Layout(LayoutDirection.VERTICAL, {
    #     "title_area": (1.0, LayoutAtom()), # 权重1,空的原子布局
    #     "content_area": (8.0, Layout(LayoutDirection.HORIZONTAL, { # 权重8,嵌套水平布局
    #         "left_area": (5.0, LayoutAtom()),
    #         "right_area": (5.0, LayoutAtom())
    #     }))
    # }).resolve(self) # self 指的是 Scene 对象,用于获取屏幕尺寸
    
    # layout["title_area"].place(title_scene02)
    # layout["content_area"]["left_area"].place(left_vgroup, aligned_edge=UL, buff=0.3)
    

    这个 Layout 类似乎是一个自定义的工具,用于将屏幕划分为不同的区域,并按比例分配空间。这对于组织复杂的场景元素非常方便,可以避免手动计算坐标。

    • 它支持垂直和水平方向的布局。
    • 可以使用字典来命名和定义区域及其相对权重或大小。
    • .resolve(self) 可能是用来根据场景的实际尺寸计算出各区域的具体边界。
    • .place(mobject, ...) 方法用于将 Manim 对象放置到指定的布局区域中,可能还支持对齐 (aligned_edge) 和边距 (buff)。

这些自定义工具虽然不是 Manim 内置的,但它们展示了如何通过封装来简化复杂场景的创建流程。在后续章节中,我们将更详细地探讨 Manim 内置的 Mobject、动画和布局方法。

本章小结

  • Manim 动画是在 Scene 类的 construct 方法中定义的。
  • 使用 manim 命令运行脚本并渲染场景。
  • 可以通过 self.camera.background_color 设置背景色。
  • Mobject.set_default()、Text.set_default() 等可以设置对象的默认属性。
  • 通过 TexTemplate 和 xelatex 可以配置 MathTex 以支持中文字体。
  • 自定义辅助函数和类(如 update_scene_number, Layout)可以提高开发效率。

在下一章,我们将深入了解 Manim 中的核心元素:Mobject,以及如何创建和操作它们。


第二章:Manim 的基石——Mobject (Manim 对象)

在 Manim 中,屏幕上显示的一切几乎都是 Mobject (Manim Object) 或其子类的实例。文本、数学公式、几何图形、SVG 图像等等,都是 Mobject。理解 Mobject 是掌握 Manim 的关键。

2.1 常见的 Mobject 类型

让我们从您提供的脚本中找出一些常见的 Mobject 类型:

  1. 文本与公式 (Text & MathTex)

    • Text("一些文本", font="字体名称", font_size=大小, color=颜色, weight=BOLD): 用于创建普通文本。
      • font: 指定字体。
      • font_size: 字体大小。
      • color: 文本颜色。
      • weight: 字体粗细,如 BOLD。
    • MathTex(r"LaTeX代码", font_size=大小, color=颜色): 用于创建使用 LaTeX 渲染的文本和数学公式。
      • r"...": Python 的原始字符串 (raw string),在包含反斜杠 \ 的 LaTeX 代码中很有用。
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    title_scene02 = Text("问题 (a): 计算转动动能", font_size=48, weight=BOLD, color=self.text_color)
    kr_formula = MathTex(r"K_R = \frac{1}{2} I \omega^2", font_size=32)
    
    # 摘自 26_trigonometric_function.txt
    angle_label = MathTex(r"\theta", color=BLACK).scale(0.8) # .scale() 用于缩放
    
  2. 几何图形 (Geometric Shapes)

    • Circle(radius=半径, color=颜色, fill_opacity=填充不透明度, fill_color=填充色, stroke_width=边框宽度)
    • Square(side_length=边长, ...)
    • Rectangle(width=宽度, height=高度, ...)
    • Polygon(点1, 点2, 点3, ..., color=颜色, ...): 通过一系列顶点定义多边形。
    • Line(起点, 终点, color=颜色, stroke_width=宽度)
    • DashedLine(起点, 终点, ...): 虚线。
    • Arrow(起点, 终点, buff=缓冲区, color=颜色, stroke_width=宽度): 箭头。
    • Dot(坐标点, radius=半径, color=颜色): 点。
    • Triangle(color=颜色, fill_opacity=填充不透明度): 等边三角形。
    # 摘自 24_why_sky_is_blue.txt
    sun = Circle(radius=1.0, color=MY_SUN_YELLOW, fill_opacity=1.0)
    prism = Triangle(color=MY_GRAY, fill_opacity=0.3).scale(0.8).rotate(PI/2)
    light_beam = Line(sun.get_right(), prism.get_left(), color=MY_YELLOW, stroke_width=0) # stroke_width=0 通常意味着不可见
    
    # 摘自 27_find the equation of the line.txt
    point1 = Dot(axes.coords_to_point(-3, 5), color=self.point_color)
    line = Line(self.axes.coords_to_point(-3, 5), self.axes.coords_to_point(2, -1), color=BLACK)
    
    # 摘自 22_refraction_of_light.txt
    interface = Line([diagram_center_x - diagram_width / 2, interface_y, 0],
                     [diagram_center_x + diagram_width / 2, interface_y, 0],
                     color=MY_DARK_TEXT)
    air_rect = Rectangle(width=diagram_width, height=air_height, fill_color=MY_LIGHT_BLUE_BG, fill_opacity=MY_AIR_ALPHA)
    

    注意:坐标通常是 NumPy 数组,例如 RIGHT (即 np.array([1,0,0])),[x, y, z]。

  3. 角度与标记 (Angles & Markers)

    • Angle(line1, line2, radius=半径, color=颜色): 创建两条线之间的角弧。
    • RightAngle(line1, line2, length=长度, color=颜色): 创建直角标记。
    • Brace(mobject, direction=方向向量, color=颜色): 在一个 Mobject旁边创建花括号。
    # 摘自 26_trigonometric_function.txt
    angle = Angle(
        Line(triangle.get_vertices()[0], triangle.get_vertices()[1]),
        Line(triangle.get_vertices()[0], triangle.get_vertices()[2]),
        color=BLACK
    )
    right_angle = RightAngle(
        Line(triangle.get_vertices()[1], triangle.get_vertices()[2]),
        Line(triangle.get_vertices()[1], triangle.get_vertices()[0]),
        color=BLACK
    )
    
    # 摘自 23_what_is_the_vector.txt
    brace_mag = Brace(vector_arrow, direction=vector_arrow.copy().rotate(PI/2).get_unit_vector(), color=BLACK)
    
  4. 坐标系 (Coordinate Systems)

    • Axes(x_range=[xmin, xmax, xstep], y_range=[ymin, ymax, ystep], x_length=长度, y_length=长度, axis_config={...}, ...): 创建二维坐标轴。
    • NumberLine(x_range=[xmin, xmax, xstep], length=长度, include_numbers=True, ...): 创建数轴。
    # 摘自 25_Parabola_equation_function_graph_changes.txt
    axes = Axes(
        x_range=[-4, 4, 1], y_range=[-7, 7, 2],
        x_length=5, y_length=5,
        axis_config={"color": BLACK, "include_tip": True, "stroke_width": 2},
        tips=True,
        x_axis_config={"stroke_width": 2, "color": BLACK},
        y_axis_config={"stroke_width": 2, "color": BLACK}
    ).add_coordinates() # add_coordinates() 会添加刻度数字
    
    # 摘自 28_find_the_value_of_function.txt
    number_line = NumberLine(
        x_range=[-6, 12, 1],
        length=8,
        include_numbers=True,
        color=BLACK
    )
    

2.2 Mobject 的属性与方法

每个 Mobject 都有一系列属性(如颜色、位置、大小)和方法(如移动、旋转、缩放)。

  • 颜色 (Color):

    • mobject.set_color(NEW_COLOR) 或 mobject.color = NEW_COLOR
    • fill_color, stroke_color
    • fill_opacity, stroke_opacity
    • 颜色可以用预定义常量(RED, BLUE, WHITE, BLACK 等)或十六进制字符串("#FF0000")表示。
    • 您提供的脚本中大量使用了自定义颜色常量,如 MY_SUN_YELLOW (来自 24_why_sky_is_blue.txt)。
  • 位置 (Positioning):

    • mobject.move_to(坐标点或另一个Mobject): 移动到指定位置。
    • mobject.shift(方向向量): 按向量平移,如 mobject.shift(UP * 2)。
    • mobject.to_edge(方向, buff=间距): 移动到屏幕边缘,如 mobject.to_edge(LEFT, buff=0.5)。
    • mobject.next_to(另一个Mobject, direction=方向, buff=间距): 放置在另一个 Mobject旁边。
    • mobject.align_to(另一个Mobject, 方向): 对齐。
    • mobject.get_center(), mobject.get_left(), mobject.get_right(), mobject.get_top(), mobject.get_bottom(): 获取 Mobject 的特定点坐标。
    # 摘自 24_why_sky_is_blue.txt
    sun = Circle(...).shift(LEFT * 5 + UP * 2) # 创建后立即平移
    atmosphere_label = Text("Atmosphere", font_size=24).next_to(atmosphere, RIGHT, buff=MED_LARGE_BUFF)
    
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    new_scene_num = Text(...).to_corner(UR, buff=MED_LARGE_BUFF)
    
  • 大小与变换 (Size & Transformation):

    • mobject.scale(缩放因子): 缩放。mobject.scale(0.5) 缩小一半。
    • mobject.rotate(角度, axis=旋转轴, about_point=旋转中心): 旋转。角度用弧度表示 (PI, DEGREES)。
    • mobject.flip(axis=翻转轴): 翻转。
    # 摘自 24_why_sky_is_blue.txt
    prism = Triangle(...).scale(0.8).rotate(PI/2) # 链式调用
    
  • Z-Index (叠放次序):

    • mobject.set_z_index(值): 控制 Mobject 的叠放顺序,值越大越靠前。
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    new_scene_num.set_z_index(100)
    

2.3 Mobject 组合:VGroup

VGroup (Vectorized Group) 是一个非常有用的 Mobject,它可以将多个 Mobject 组合成一个单一的 Mobject。对 VGroup 进行的操作(如移动、缩放、变色)会同时应用于其包含的所有子 Mobject。

# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
left_vgroup = VGroup(
    kr_formula_text, kr_formula,
    conversion_intro_text, omega_given, omega_conversion_formula, omega_conversion_calc, omega_converted_value,
).arrange(DOWN, buff=0.25, aligned_edge=LEFT) # arrange 用于排列组内元素

# 播放动画时,可以对整个组进行操作
self.play(FadeOut(left_vgroup))
  • VGroup(mobj1, mobj2, ...): 创建一个包含这些 Mobject 的组。
  • vgroup.add(mobj): 向组中添加 Mobject。
  • vgroup.remove(mobj): 从组中移除 Mobject。
  • vgroup.arrange(direction, buff=间距, aligned_edge=对齐边缘): 自动排列组内的 Mobject。
    • direction: 如 DOWN, RIGHT, UP, LEFT。
    • aligned_edge: 如 LEFT, RIGHT, CENTER。

2.4 坐标与向量常量

Manim 提供了一些方便的向量常量来表示方向和位置:

  • ORIGIN: np.array([0,0,0]),原点。
  • UP: np.array([0,1,0])
  • DOWN: np.array([0,-1,0])
  • LEFT: np.array([-1,0,0])
  • RIGHT: np.array([1,0,0])
  • UL: UP + LEFT (左上)
  • DR: DOWN + RIGHT (右下)
  • PI: 数学常数 π。
  • DEGREES: PI / 180,用于将角度转换为弧度 (例如 30 * DEGREES)。
  • TAU: 2 * PI。

2.5 ValueTracker 和 always_redraw (动态更新)

当需要 Mobject 根据某个变化的值动态更新时,ValueTracker 和 always_redraw 非常有用。这在 25_Parabola_equation_function_graph_changes.txt 中有很好的体现。

# 摘自 25_Parabola_equation_function_graph_changes.txt
b_tracker = ValueTracker(0) # 创建一个值为0的追踪器

# label 会根据 b_tracker 的值自动重绘
label = always_redraw(
    lambda: MathTex(f"b = {b_tracker.get_value():.1f}", color=PURPLE)
)
# parabola 也会根据 b_tracker 的值自动重绘
parabola = always_redraw(
    lambda: axes.plot(
        lambda x: a_val * x ** 2 + b_tracker.get_value() * x + c_val,
        color=PURPLE,
        x_range=plot_x_range
    )
)
self.add(parabola, label) # 添加到场景后,它们会在每一帧自动更新

# 改变 b_tracker 的值,label 和 parabola 会随之变化
self.play(b_tracker.animate.set_value(3), run_time=3.0)
  • ValueTracker(initial_value): 创建一个可以追踪数值变化的对象。
  • tracker.get_value(): 获取追踪器的当前值。
  • tracker.set_value(new_value): 设置追踪器的新值 (通常在动画中使用 tracker.animate.set_value(...))。
  • always_redraw(lambda_function): 创建一个 Mobject,该 Mobject 的外观由 lambda_function 返回的 Mobject 决定。这个函数会在每一帧被调用,从而实现动态更新。

本章小结

  • Mobject 是 Manim 中所有可见元素的基础。
  • Manim 提供了丰富的 Mobject 类型,包括文本、公式、几何图形和坐标系。
  • Mobject 具有颜色、位置、大小等属性,并可以通过方法进行变换。
  • VGroup 用于组合和管理多个 Mobject。
  • ValueTracker 和 always_redraw 可以创建动态更新的 Mobject。

在下一章,我们将学习如何让这些 Mobject 动起来,探索 Manim 强大的动画系统。


第三章:赋予生命——Manim 动画

Manim 的核心魅力在于其强大的动画能力。通过 self.play() 方法,我们可以将 Mobject 的创建、变换和消失过程以动画形式展现出来。

3.1 self.play() 的基本用法

self.play(Animation1, Animation2, ..., run_time=秒数, rate_func=速率函数)

  • Animation: Manim 中的动画类实例,例如 Write(text_mobject)。
  • 可以同时播放多个动画,它们会并行执行。
  • run_time: 动画的持续时间,单位是秒。默认通常是1秒。
  • rate_func: 控制动画的速率曲线,例如:
    • linear: 线性,匀速。
    • smooth: 平滑,两头慢中间快 (默认)。
    • rush_into: 开始快,然后减速。
    • rush_from: 开始慢,然后加速。
    • there_and_back: 先正向再反向。
    • 等等...

3.2 常见的动画类型

  1. 创建与书写 (Creation & Writing)

    • Create(mobject): 逐渐显示一个 Mobject,通常用于几何图形。
    • Write(text_or_mathtex_mobject): 模拟笔迹书写文本或公式。
    • DrawBorderThenFill(mobject): 先绘制边框,然后填充。
    • ShowCreation(mobject): 与 Create 类似,但有时效果略有不同。
    # 摘自 29_find_angles_of_triangles.txt
    title = Text("Triangle Angle Problem", font_size=38, color=BLACK)
    problem = Text("In triangle ABC...", font_size=28, color=BLACK)
    triangle = Triangle(fill_opacity=0.2, color=self.triangle_color)
    
    self.play(Write(title), run_time=1)
    self.play(Write(problem), run_time=2)
    self.play(Create(triangle), run_time=1.5)
    
    # 摘自 27_find the equation of the line.txt
    # self.play(Create(axes), run_time=1)
    # self.play(Create(line), run_time=1.5)
    
  2. 消失与移除 (Fading & Removal)

    • FadeOut(mobject): 将 Mobject 淡出。
    • Uncreate(mobject): Create 的逆向动画。
    • FadeOutAndShift(mobject, direction=向量): 淡出并向指定方向移动。
    # 摘自 29_find_angles_of_triangles.txt
    # previous_objects = VGroup(self.problem, self.triangle, self.labels)
    # self.play(FadeOut(previous_objects), run_time=1)
    
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    # if self.current_scene_num_mob:
    #     animations.append(FadeOut(self.current_scene_num_mob, run_time=0.5))
    
  3. 引入 (Introduction)

    • FadeIn(mobject): 将 Mobject 淡入。
    • FadeInFrom(mobject, direction=向量): 从指定方向淡入。
    • GrowFromCenter(mobject): 从中心放大出现。
    • GrowFromEdge(mobject, edge=方向): 从指定边缘生长出现。
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    # self.play(FadeIn(kr_formula_text, shift=UP * 0.2), run_time=0.7)
    # shift=UP*0.2 是 FadeIn 的一个参数,表示淡入时向上轻微移动
    
    # 摘自 23_what_is_the_vector.txt
    # self.play(FadeIn(desc_group[0]), GrowFromCenter(brace_mag), FadeIn(brace_mag_label), run_time=2.0)
    
  4. 变换 (Transformation)

    • Transform(mobject1, mobject2): 将 mobject1 平滑地变成 mobject2。mobject1 会从场景中移除,mobject2 会被添加。
    • ReplacementTransform(mobject1, mobject2): 与 Transform 类似,但更强调替换的语义。
    • TransformMatchingTex(mathtex1, mathtex2): 专门用于 MathTex 对象的变换,它会尝试匹配 LaTeX 字符串中相同的部分,使变换更自然。
    • TransformMatchingShapes(shape1, shape2): 类似地,用于形状的智能匹配变换。
    # 摘自 25_Parabola_equation_function_graph_changes.txt
    # graph = axes.plot(lambda x: x ** 2, color=BLUE, x_range=plot_x_range)
    # graph_a2 = axes.plot(lambda x: 2 * x ** 2,  x_range=plot_x_range)
    # label = MathTex("a=1", color=BLUE)
    # label_a2 = MathTex("a=2", color=GREEN)
    # self.play(Transform(graph, graph_a2), Transform(label, label_a2), run_time=1.5)
    
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    # omega_conversion_formula = MathTex(r"\omega (\text{rad/s}) = ...", font_size=32)
    # omega_conversion_calc = MathTex(r"= 1200 \times \frac{2\pi}{60} ...", font_size=32)
    # self.play(TransformMatchingTex(omega_conversion_formula.copy(), omega_conversion_calc), run_time=1.2)
    # 注意 .copy() 的使用:Transform 会消耗原始对象,如果后续还需要原始对象,或者要基于原始对象进行多次变换,就需要复制。
    
  5. 移动与路径 (Movement & Paths)

    • mobject.animate.shift(向量)
    • mobject.animate.move_to(目标点)
    • mobject.animate.scale(缩放因子)
    • mobject.animate.rotate(角度)
    • mobject.animate.set_color(颜色)
    • MoveAlongPath(mobject, path_mobject): 让 Mobject 沿着一个路径 Mobject (如 Line, Arc, VMobject) 移动。
    # 假设有一个 circle 对象
    # self.play(circle.animate.shift(RIGHT * 2).scale(0.5).set_color(RED), run_time=2)
    # .animate 语法允许我们将 Mobject 的属性修改方法变成动画。
    
    # 摘自 25_Parabola_equation_function_graph_changes.txt (ValueTracker 驱动的动画)
    # b_tracker = ValueTracker(0)
    # self.play(b_tracker.animate.set_value(3), run_time=3.0)
    # 这里是 ValueTracker 对象的 .animate 接口
    
  6. 强调 (Emphasis)

    • Indicate(mobject, color=高亮色, scale_factor=缩放因子): 短暂高亮一个 Mobject (通常是放大一点并变色,然后恢复)。
    • Flash(mobject_or_point, color=闪光色, flash_radius=半径): 在 Mobject 或某点处产生一个闪光效果。
    • FocusOn(mobject_or_point): 将镜头焦点移向某处。
    • Circumscribe(mobject, color=圆圈颜色): 用一个圆圈框住 Mobject。
    # 摘自 22_refraction_of_light.txt
    # self.play(Indicate(snell_law_formula.get_part_by_tex("n_1"), color=MY_DARK_TEXT), run_time=0.8)
    
    # 摘自 27_find the equation of the line.txt
    # self.play(
    #     Flash(self.point1, color=self.point_color, flash_radius=0.5),
    #     Flash(self.point2, color=self.point_color, flash_radius=0.5),
    #     run_time=1.5
    # )
    

3.3 动画组与同步

  • AnimationGroup(*animations, lag_ratio=延迟比例): 将多个动画组合在一起播放。lag_ratio 控制组内动画的启动延迟。

    • lag_ratio = 0: 所有动画同时开始。
    • lag_ratio = 1: 动画依次播放,一个结束后下一个开始。
    • 0 < lag_ratio < 1: 动画之间有部分重叠。
    # 摘自 22_refraction_of_light.txt
    # self.play(AnimationGroup(
    #     Create(red_ray),
    #     Create(green_ray),
    #     Create(blue_ray),
    #     lag_ratio=0.3 # 红、绿、蓝光线依次出现,但有重叠
    # ), run_time=2.0)
    
    # 摘自 24_why_sky_is_blue.txt
    # self.play(AnimationGroup(*[FadeIn(p, shift=UP*0.1) for p in summary_group], lag_ratio=0.3), run_time=4.0)
    # 列表推导式与 * 解包操作,优雅地为 VGroup 中的每个元素创建 FadeIn 动画
    
  • 同步语音与动画: 您提供的脚本中广泛使用了 custom_voiceover_tts 上下文管理器来同步语音和动画。这是一个非常实用的自定义工具。其核心思想是:

    1. 获取语音的持续时间 tracker.duration。
    2. 计算动画的总持续时间 anim_duration。
    3. 通过 self.wait(max(0, tracker.duration - anim_duration)) 来确保动画播放的总时长与语音时长匹配。如果动画比语音短,就等待差值;如果动画比语音长,则语音可能会提前结束。
    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    # voice_text_s02_p1 = "对于问题 (a),..."
    # with custom_voiceover_tts(voice_text_s02_p1) as tracker:
    #     if tracker.audio_path and tracker.duration > 0: self.add_sound(tracker.audio_path)
    #     self.play(FadeIn(kr_formula_text, shift=UP * 0.2), run_time=0.7)
    #     self.play(Write(kr_formula), run_time=1.2)
    #     anim_duration = 0.7 + 1.2
    #     wait_time = max(0.001, tracker.duration - anim_duration) if tracker.duration > 0 else 0.5
    #     self.wait(wait_time)
    

    虽然 custom_voiceover_tts 是自定义的,但这种同步逻辑是通用的。

3.4 self.add() 与 self.remove()

有时候,我们希望一个 Mobject 立即出现在屏幕上或从屏幕上消失,而不需要动画效果。

  • self.add(mobject1, mobject2, ...): 将一个或多个 Mobject 添加到场景中,它们会立即显示。
  • self.remove(mobject1, mobject2, ...): 将一个或多个 Mobject 从场景中移除,它们会立即消失。

这在管理动态更新的 Mobject (如 always_redraw 创建的) 或在复杂动画序列中切换元素时很有用。

# 摘自 25_Parabola_equation_function_graph_changes.txt
# label = always_redraw(...)
# parabola = always_redraw(...)
# self.add(parabola, label) # 添加后,它们会在每一帧根据 ValueTracker 自动更新

本章小结

  • self.play() 是执行动画的核心方法。
  • Manim 提供了丰富的动画类型,用于创建、消失、变换和强调 Mobject。
  • .animate 语法提供了一种便捷的方式来动画化 Mobject 的属性变化。
  • AnimationGroup 可以组合多个动画并控制它们的播放顺序。
  • 通过计算动画和语音时长,可以实现两者之间的同步。
  • self.add() 和 self.remove() 用于即时显示或隐藏 Mobject。

下一章,我们将学习如何在 Manim 中绘制图形和函数,这对于数学可视化至关重要。


第四章:绘制图形与函数——坐标系的力量

数学可视化离不开坐标系和函数图像。Manim 提供了强大的工具来创建和操作它们。

4.1 Axes:二维坐标系

Axes 类用于创建标准的二维笛卡尔坐标系。

  • 基本创建:

    # 摘自 25_Parabola_equation_function_graph_changes.txt
    axes = Axes(
        x_range=[-4, 4, 1],  # x轴范围:[最小值, 最大值, 步长]
        y_range=[-7, 7, 2],  # y轴范围
        x_length=5,          # x轴在屏幕上的显示长度
        y_length=5,          # y轴在屏幕上的显示长度
        axis_config={"color": BLACK, "include_tip": True, "stroke_width": 2}, # 坐标轴通用配置
        tips=True,           # 是否显示箭头 (Manim CE 中 tips 默认为 True, include_tip 更常用)
        x_axis_config={"stroke_width": 2, "color": BLACK}, # x轴特定配置
        y_axis_config={"stroke_width": 2, "color": BLACK}  # y轴特定配置
    ).add_coordinates() # .add_coordinates() 会自动添加刻度数字
    
    • x_range 和 y_range 定义了坐标系的数学范围和刻度步长。
    • x_length 和 y_length 定义了坐标轴在屏幕上的物理长度。
    • axis_config 可以统一配置两条轴的样式。
    • x_axis_config 和 y_axis_config 可以分别配置 x 轴和 y 轴。
    • .add_coordinates() 是一个方便的方法,用于在轴上添加刻度值。你也可以通过 x_axis_config={"include_numbers": True} 来实现。
  • 坐标轴标签: 通常使用 Text 或 MathTex 创建标签,然后用 .next_to() 定位。

    # 摘自 27_find the equation of the line.txt
    x_label = Text("x")
    y_label = Text("y")
    x_label.next_to(axes.x_axis.get_end(), RIGHT)
    y_label.next_to(axes.y_axis.get_end(), UP)
    

    axes.x_axis 和 axes.y_axis 是 Axes 对象内部的 NumberLine 对象,代表 x 轴和 y 轴本身。

  • 手动添加刻度标签 (如果不想用 add_coordinates 或需要更精细的控制): 26_trigonometric_function.txt 和 21_Geometric_Meaning_of_Euler_s_Formula.txt 展示了如何手动为 Axes 添加刻度标签,特别是当标签包含 π 时。

    # 摘自 26_trigonometric_function.txt (略作简化)
    axes = Axes(
        x_range=[0, 2 * PI + 0.1, PI / 2],
        y_range=[-1.5, 1.5, 1],
        # ...
        axis_config={"include_numbers": False}, # 禁用自动数字
    )
    x_labels_mobs = VGroup(*( # 使用 VGroup 组合所有标签
        MathTex(tex_str, font_size=24, color=BLACK)
            .next_to(axes.c2p(x_val, 0), DOWN, buff=SMALL_BUFF)
        for x_val, tex_str in ( # 元组列表,定义 (数学值, LaTeX字符串)
            (0, r"0"),
            (PI / 2, r"\frac{\pi}{2}"),
            (PI, r"\pi"),
            (3 * PI / 2, r"\frac{3\pi}{2}"),
            (2 * PI, r"2\pi"),
        )
    ))
    # y_labels_mobs 类似创建
    self.play(Create(axes), Write(x_labels_mobs), Write(y_labels_mobs))
    

    这里使用了 axes.c2p(x, y) 方法,见下节。

4.2 坐标转换:coords_to_point (c2p) 和 point_to_coords (p2c)

Axes 对象提供了在数学坐标和屏幕坐标之间转换的方法:

  • axes.coords_to_point(x, y) 或 axes.c2p(x, y): 将数学坐标 (x, y) 转换为屏幕上的点 (一个 NumPy 数组 [screen_x, screen_y, 0])。
  • axes.point_to_coords(point) 或 axes.p2c(point): 将屏幕上的点转换回数学坐标。

这在需要在坐标系上精确放置 Dot、Text 或其他 Mobject 时非常有用。

# 摘自 27_find the equation of the line.txt
point1_coords = (-3, 5)
point1_on_screen = axes.coords_to_point(point1_coords[0], point1_coords[1])
point1_dot = Dot(point1_on_screen, color=self.point_color)

# 摘自 21_Geometric_Meaning_of_Euler_s_Formula.txt (单位圆上的点)
# theta_value = PI / 3
# point_x = np.cos(theta_value)
# point_y = np.sin(theta_value)
# point_P_coord_on_screen = axes.c2p(point_x, point_y)
# self.point_P = Dot(point_P_coord_on_screen, color=BLUE)

4.3 绘制函数图像:axes.plot()

axes.plot(function, x_range=[xmin, xmax], color=颜色, ...) 方法用于绘制函数图像。

# 摘自 25_Parabola_equation_function_graph_changes.txt
# a_val = 1, b_val = 0, c_val = 0
parabola_function = lambda x: a_val * x ** 2 + b_val * x + c_val
graph = axes.plot(parabola_function, color=BLUE, x_range=[-2.5, 2.5])
self.play(Create(graph))
  • function: 一个 Python lambda 函数或普通函数,接受一个参数 (x 值) 并返回 y 值。
  • x_range: 可选参数,指定绘制函数的 x 值范围。如果未提供,则使用 Axes 的 x_range。

4.4 NumberLine:数轴

NumberLine 用于创建一维数轴。它的配置与 Axes 中的单轴类似。

# 摘自 28_find_the_value_of_function.txt
number_line = NumberLine(
    x_range=[-6, 12, 1],    # 数学范围和步长
    length=8,               # 屏幕显示长度
    include_numbers=True,   # 显示刻度数字
    color=BLACK
)
# 在数轴上标点
x_point_math_coord = 4
x_point_screen_coord = number_line.n2p(x_point_math_coord) # number_to_point
x_dot = Dot(x_point_screen_coord, color=BLACK)
x_label = MathTex("x = 4").next_to(x_dot, DOWN)
  • number_line.n2p(number): 将数轴上的一个数字转换为屏幕上的点。
  • number_line.p2n(point): 将屏幕上的点转换为数轴上的数字。

4.5 单位圆示例 (结合 Axes 和 Circle)

21_Geometric_Meaning_of_Euler_s_Formula.txt 和 26_trigonometric_function.txt 都展示了如何在 Axes 上绘制单位圆。

# 摘自 26_trigonometric_function.txt (单位圆部分)
axes = Axes(
    x_range=[-1.5, 1.5, 1], y_range=[-1.5, 1.5, 1], x_length=5, y_length=5,
    # ...
)
# 单位圆的半径是1 (数学单位)。需要将其转换为屏幕单位。
# axes.x_axis.unit_size 是 x 轴上一个数学单位对应的屏幕长度。
# axes.y_axis.unit_size 是 y 轴上一个数学单位对应的屏幕长度。
# 如果 x_length/x_range_span != y_length/y_range_span,那么 x 和 y 的 unit_size 可能不同,圆会变形。
# 通常,我们会确保 x_length / (xmax-xmin) == y_length / (ymax-ymin) 来保持比例。
# 或者,如果 Axes 的 x_length 和 y_length 相等,且 x_range 和 y_range 的跨度也设计成相等,
# 那么可以直接使用 x_axis.unit_size 或 y_axis.unit_size。
# 一个更稳妥的方式是,如果圆心在原点:
radius_on_screen = np.linalg.norm(axes.c2p(1, 0) - axes.c2p(0, 0)) # 计算原点到(1,0)的屏幕距离
circle = Circle(radius=radius_on_screen, stroke_width=2).move_to(axes.c2p(0,0)) # 圆心在原点

# 动态点 P 在单位圆上
theta_tracker = ValueTracker(0) # 角度追踪器
point_P = Dot(color=BLUE)
point_P.add_updater(
    lambda m: m.move_to(axes.c2p(np.cos(theta_tracker.get_value()), np.sin(theta_tracker.get_value())))
)
# 其他元素如半径、投影线、角度标签等也使用 add_updater 动态更新

这个例子展示了如何结合 Axes 的坐标转换、Circle 的创建以及 ValueTracker 和 add_updater (或 always_redraw) 来实现动态的单位圆可视化。

4.6 绘制参数方程图像:axes.plot_parametric_curve()

虽然您提供的脚本中没有直接使用 plot_parametric_curve,但它是绘制更复杂曲线(如圆、椭圆、螺旋线等)的有用工具。 axes.plot_parametric_curve(function, t_range=[tmin, tmax, tstep], ...)

  • function: 一个接受参数 t 并返回 (x, y)坐标对的函数。

例如,绘制单位圆:

unit_circle_parametric = axes.plot_parametric_curve(
    lambda t: np.array([np.cos(t), np.sin(t), 0]), # 返回 [x,y,z]
    t_range=[0, TAU, 0.01], # t 从 0 到 2π
    color=RED
)

本章小结

  • Axes 类用于创建二维坐标系,可以配置范围、长度和样式。
  • axes.c2p() 和 axes.p2c() 在数学坐标和屏幕坐标间转换。
  • axes.plot() 用于绘制标准函数 y = f(x) 的图像。
  • NumberLine 用于创建数轴,n2p() 和 p2n() 进行坐标转换。
  • 结合 Circle、ValueTracker 和 updater 可以在 Axes 上创建动态的单位圆。
  • axes.plot_parametric_curve() 可以绘制参数方程定义的曲线。

在下一章,我们将探讨如何组织和管理复杂的场景,特别是当场景中包含大量元素或多个动画阶段时。


第五章:组织复杂场景——布局与管理

当动画变得复杂,包含许多元素或多个阶段时,良好的组织结构至关重要。本章将探讨如何使用 VGroup、自定义布局类以及将场景分解为多个方法来管理复杂性。

5.1 VGroup:Mobject 的容器

我们之前已经接触过 VGroup,它是组织多个 Mobject 的基本工具。

  • 集中控制:对 VGroup 进行的变换(移动、缩放、旋转、变色)会应用到其所有子 Mobject。
  • 批量动画:可以对整个 VGroup 应用一个动画,如 FadeIn(my_vgroup)。
  • 排列:vgroup.arrange(direction, buff, aligned_edge) 方法可以方便地排列组内元素。
# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
left_vgroup = VGroup(
    kr_formula_text, kr_formula,
    conversion_intro_text, omega_given, omega_conversion_formula, omega_conversion_calc, omega_converted_value,
).arrange(DOWN, buff=0.25, aligned_edge=LEFT)

right_vgroup = VGroup(
    kr_calc_intro_text, kr_substitution, kr_calc_step1, kr_calc_step2, kr_final_exact,
    kr_approx_intro, kr_approx_calc, kr_approx_value
).arrange(DOWN, buff=0.25, aligned_edge=LEFT)

# 之后可以将这些 VGroup 放置到布局区域
# layout["content_area"]["left_area"].place(left_vgroup, aligned_edge=UL, buff=0.3)
# layout["content_area"]["right_area"].place(right_vgroup, aligned_edge=UL, buff=0.3)

这里,多个 Text 和 MathTex 对象被分别组合到 left_vgroup 和 right_vgroup 中,并使用 arrange 方法进行垂直排列和左对齐。

5.2 自定义 Layout 类:屏幕区域划分

您提供的许多脚本(如 31_..., 25_..., 22_..., 24_..., 23_..., 26_..., 27_..., 28_..., 29_..., 30_..., 21_...)都使用了一个名为 Layout 的自定义类。这个类(通常在 manim_utils.py 中定义,这里我们根据用法推断其功能)极大地简化了屏幕空间的管理。

  • 目的:将屏幕划分为命名区域,并按比例分配空间,使得 Mobject 可以方便地放置到这些区域中,而无需手动计算绝对坐标。
  • 典型用法:
    1. 定义布局结构:使用嵌套字典描述区域的层次、方向(垂直/水平)和相对权重/大小。
    2. 解析布局:调用 .resolve(self)(self 指场景对象)来根据当前屏幕尺寸计算各区域的实际边界。
    3. 放置 Mobject:使用 layout["area_name"].place(mobject, ...) 将 Mobject 放入指定区域。
# 摘自 25_Parabola_equation_function_graph_changes.txt
# 假设 Layout, LayoutAtom, LayoutDirection 已定义
layout = Layout(LayoutDirection.VERTICAL, { # 顶层是垂直布局
    "title": (1.5, LayoutAtom()), # "title"区域,权重1.5,是一个原子布局单元
    "body": (7.0, Layout(LayoutDirection.HORIZONTAL, { # "body"区域,权重7.0,内部是水平布局
        "text_group": (5.0, LayoutAtom()), # "body"内的"text_group"区域,权重5.0
        "graph_group": (3.0, Layout(LayoutDirection.VERTICAL, { # "body"内的"graph_group"区域,权重3.0,内部垂直
            "formula": (1.0, LayoutAtom()),
            "label": (1.0, LayoutAtom()),
            "graph": (6.0, LayoutAtom()),
        })),
    })),
}).resolve(self) # self 是场景对象

title_mobject = Title("Parabola Equation Changes")
layout["title"].place(title_mobject) # 将 title_mobject 放入 "title" 区域

text_content = VGroup(...)
layout["body"]["text_group"].place(text_content, aligned_edge=UL, buff=0.5) # 放入嵌套区域

axes_mobject = Axes(...)
layout["body"]["graph_group"]["graph"].place(axes_mobject) # 放入更深层嵌套区域
  • LayoutDirection.VERTICAL / LayoutDirection.HORIZONTAL: 指定子区域是垂直排列还是水平排列。
  • LayoutAtom(): 表示一个基本的、不可再分的布局单元。
  • 字典的键是区域名称,值是一个元组 (weight, sub_layout_or_atom)。weight 用于按比例分配空间。
  • .place() 方法通常还允许指定对齐方式 (aligned_edge) 和边距 (buff)。

虽然这个 Layout 类是自定义的,但它体现了一种重要的设计模式:声明式布局。你描述了你想要的布局结构,而不是通过命令式地计算每个元素的坐标。这使得代码更易读、更易维护,并且能更好地适应不同的屏幕比例。

5.3 将场景分解为多个方法

对于较长的动画,将所有逻辑都放在一个 construct 方法中会使其变得臃肿难读。一个好的做法是将动画分解为多个逻辑阶段,每个阶段用一个单独的方法实现。

您提供的脚本广泛采用了这种模式,例如:

  • 29_find_angles_of_triangles.txt: play_scene_01(), play_scene_02(), play_scene_03(), play_scene_04()
  • 25_Parabola_equation_function_graph_changes.txt: play_section_01(), play_section_02(), ...
  • 22_refraction_of_light.txt: play_scene_01(), play_scene_04(), play_scene_05() (注意编号不连续,但逻辑上是分段的)
# 摘自 29_find_angles_of_triangles.txt
class CombinedScene(Scene):
    def __init__(self):
        super().__init__()
        self.current_scene_num_mob = None
        # ... 其他初始化 ...

    def construct(self):
        self.camera.background_color = WHITE
        # 按顺序调用各个场景片段
        self.play_scene_01()  # Problem statement
        self.play_scene_02()  # Setup equation
        self.play_scene_03()  # Solve equation
        self.play_scene_04()  # Draw triangle with correct angles
        self.fadeout_all()    # Clean fadeout at the end

    def update_scene_number(self, number_str):
        # ... (如第一章所述) ...
        pass

    def play_scene_01(self):
        self.update_scene_number("01")
        # ... 场景1的逻辑 ...
        # 将场景1中创建的关键 Mobject 保存为 self.属性,以便后续场景片段使用
        self.title = title_mobject
        self.problem = problem_mobject
        # ...
        pass

    def play_scene_02(self):
        self.update_scene_number("02")
        # ... 场景2的逻辑 ...
        # 可能需要 FadeOut 场景1中的某些元素
        # self.play(FadeOut(self.problem), ...)
        # 然后创建和动画化场景2的元素
        pass

    # ... 其他 play_scene_XX 方法 ...

    def fadeout_all(self):
        """Fadeout all elements for a clean ending."""
        self.play(FadeOut(*self.mobjects), run_time=1.5) # *self.mobjects 获取场景中所有顶层 Mobject
  • 主 construct 方法: 变得非常简洁,只负责按顺序调用各个片段方法。
  • 片段方法 (play_scene_XX): 每个方法负责一部分动画逻辑。
    • 通常以 self.update_scene_number() (如果使用) 开始。
    • 创建和动画化该片段所需的 Mobject。
    • 如果需要,将关键 Mobject 保存为 self.attribute_name,以便其他片段方法可以访问或移除它们。
    • 在片段开始时,可能需要 FadeOut 上一个片段中不再需要的元素。
  • 状态管理: 通过 self.attribute_name 在不同片段方法之间共享 Mobject 状态。
  • 清理: fadeout_all() 或类似方法用于在整个动画结束时清理屏幕。

5.4 清理屏幕元素 (clear_section / clear_and_reset)

在分段场景中,管理哪些元素应该保留、哪些应该移除是很重要的。您提供的 22_refraction_of_light.txt 和 23_what_is_the_vector.txt 中有名为 clear_and_reset 或 clear_section 的方法。

# 摘自 22_refraction_of_light.txt
class CombinedScene(Scene):
    def setup(self):
        # ...
        self.diagram_elements = VGroup() # 用于存储当前“部分”的元素

    def clear_and_reset(self):
        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() # 清除 updater,防止错误

        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() # Manim Scene 的方法,移除所有 mobjects 和 updaters

        self.diagram_elements = VGroup() # 重置存储器
        self.wait(0.1)

    def play_scene_01(self):
        self.update_scene_number("01")
        # ... 创建 title, interface, media_bg, media_labels ...
        # self.diagram_elements.add(title, interface, media_bg, ...) # (示例,原脚本可能直接操作 self.mobjects)
        # ... 动画 ...
        pass # 结束后不清理,元素保留到下一个 clear_and_reset 调用

    def construct(self):
        self.play_scene_01()
        self.clear_and_reset() # 清理场景1的元素

        self.play_scene_04()
        self.clear_and_reset() # 清理场景4的元素
        # ...

23_what_is_the_vector.txt 中的 clear_section 稍微不同,它操作的是一个名为 self.section_elements 的 VGroup,只清理这个组中的元素,而不是整个屏幕。这允许在不同“部分”之间保留一些共同的元素(如标题)。

# 摘自 23_what_is_the_vector.txt
class CombinedScene(Scene):
    def setup(self):
        # ...
        self.section_elements = VGroup() # 用于存储当前“部分”的元素

    def clear_section(self):
        # ... (清除 self.section_elements 中的 updater) ...
        valid_elements = [elem for elem in self.section_elements if elem is not None]
        if valid_elements:
            self.play(FadeOut(Group(*valid_elements)), run_time=0.75)
        self.section_elements = VGroup() # 重置
        self.wait(0.1)

    def play_scene_01(self): # 介绍部分
        self.update_scene_number("01")
        title = Text("What is a Vector?")
        # self.add(title) # 将标题直接添加到场景,不加入 self.section_elements
        definition_text = Text("A vector is ...")
        vector_arrow = Arrow(...)
        self.section_elements.add(definition_text, vector_arrow) # 这些会被 clear_section 清理
        # ... 动画 ...
        pass

    def construct(self):
        self.play_scene_01()
        self.clear_section() # 清理 definition_text 和 vector_arrow,但 title 还在
        # ...

选择哪种清理方式取决于你的具体需求。

本章小结

  • VGroup 是组织和批量操作 Mobject 的基础。
  • 自定义 Layout 类(或类似的机制)有助于声明式地管理屏幕空间,使复杂布局更易于实现和维护。
  • 将 construct 方法分解为多个 play_scene_XX 或 play_section_XX 方法,可以提高代码的可读性和模块性。
  • 在分段动画中,需要仔细管理哪些 Mobject 在何时被添加、移除或在不同片段间共享。自定义的清理方法可以帮助实现这一点。

下一章,我们将关注 Manim 的高级定制选项,包括颜色、字体(特别是 CJK)、以及 TexTemplate 的使用。


第六章:个性化你的动画——颜色、字体与模板

Manim 提供了丰富的自定义选项,让你的动画独具风格。本章将重点介绍如何使用自定义颜色、配置字体(特别是中文字体),以及利用 TexTemplate 来精细控制 LaTeX 渲染。

6.1 自定义颜色

Manim 内置了许多颜色常量 (如 RED, BLUE, GREEN, WHITE, BLACK, YELLOW, PURPLE, ORANGE, PINK, TEAL, GOLD 等)。但为了动画风格的统一或特殊需求,你经常会定义自己的颜色集。

您提供的脚本中,22_refraction_of_light.txt 和 24_why_sky_is_blue.txt 是很好的例子:

# 摘自 22_refraction_of_light.txt
# --- Custom Colors ---
MY_LIGHT_BLUE_BG = "#E3F2FD"  # Light blue background
MY_WATER_BLUE = "#90CAF9"    # Water medium color
MY_AIR_ALPHA = 0.0           # Air is transparent
MY_DARK_TEXT = "#1E293B"     # Dark text color
MY_HIGHLIGHT_RED = "#E53935" # Incident angle, etc.
MY_RAY_YELLOW = "#FFCA28"    # Light ray color
# ... 其他颜色定义 ...

class CombinedScene(Scene):
    def setup(self):
        # ...
        self.camera.background_color = MY_LIGHT_BLUE_BG # 使用自定义背景色
        # ...

    def play_scene_01(self):
        # ...
        title = Title("Refraction of Light", font_size=48, color=MY_DARK_TEXT) # 使用自定义文本颜色
        interface = Line(..., color=MY_DARK_TEXT)
        water_rect = Rectangle(..., fill_color=MY_WATER_BLUE, fill_opacity=0.3)
        incident_ray = Arrow(..., color=MY_RAY_YELLOW)
        angle_inc_arc = Arc(..., color=MY_HIGHLIGHT_RED)
        # ...
  • 定义方式:通常在脚本的开头将颜色值赋给大写常量名。颜色值可以是:
    • 十六进制字符串:如 "#E3F2FD"。
    • Manim 预定义颜色:如 BLUE。
    • RGB 元组:如 (1.0, 0.5, 0.0) (每个分量在 0 到 1 之间)。
  • 使用:在创建 Mobject 或设置其属性时,直接使用这些自定义常量。
  • 好处:
    • 一致性:确保整个动画或系列动画的颜色风格统一。
    • 易修改:如果想改变某个主题色,只需修改顶部的定义即可,无需在代码中多处查找替换。
    • 可读性:MY_WATER_BLUE 比 "#90CAF9" 更能表达颜色的用途。

6.2 字体配置

  1. Text 对象的字体: 对于通过 Text 创建的 Mobject(不使用 LaTeX 渲染),可以直接在创建时或通过 Text.set_default() 指定字体。

    # 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
    # from manim_utils import get_available_font # 假设的工具函数
    
    class CombinedScene(Scene):
        def __init__(self, **kwargs):
            super().__init__(**kwargs)
            # ...
            final_font = get_available_font() # 获取一个系统可用的字体名称
            if final_font:
                Text.set_default(font=final_font) # 设置 Text 对象的默认字体
            # ...
    
    # 单独设置
    my_text = Text("特定文本", font="Arial")
    

    get_available_font() 是一个自定义函数,它的作用可能是检查系统中安装了哪些字体,并返回一个可用的字体名称。你需要确保指定的字体名称是你的操作系统和 Manim (底层依赖 Pango/Cairo) 能够识别的。对于中文字体,可以是 "SimSun", "Microsoft YaHei", "PingFang SC", "Songti SC" 等。

  2. MathTex 和 Tex 对象的字体 (通过 LaTeX): 当文本或公式需要通过 LaTeX 渲染时(主要是为了数学排版或复杂的 CJK 字符支持),字体配置就涉及到 TexTemplate。

6.3 TexTemplate:自定义 LaTeX 环境

Manim 使用 LaTeX 来渲染 MathTex 和 Tex 对象。通过 TexTemplate,你可以完全控制用于渲染的 LaTeX 文档的导言区 (preamble) 和编译器。

这对于支持特定语言(如中文)、使用特殊 LaTeX 宏包或自定义文档类非常重要。

# 摘自 31_rotational_kinetic_energy_and_angular_momentum.txt
CJK_FONT_NAME = "Songti SC" # 例如 "SimHei" 或 "Source Han Sans CN"
cjk_template = TexTemplate(
    tex_compiler="xelatex", # 推荐使用 xelatex 处理 CJK
    output_format=".xdv",   # xelatex 的输出格式
    preamble=rf"""
\usepackage{{amsmath}}
\usepackage{{amssymb}}
\usepackage{{fontspec}}     % 允许使用系统字体
\usepackage{{xeCJK}}        % xelatex 的 CJK 处理宏包
\setCJKmainfont{{{CJK_FONT_NAME}}} % 设置 CJK 主字体
% 你还可以添加其他需要的宏包,例如:
% \usepackage{physics} % 物理公式常用
% \usepackage{chemfig} % 化学结构式
"""
)

class CombinedScene(Scene):
    def __init__(self, **kwargs):
        # ...
        MathTex.set_default(tex_template=cjk_template) # 将此模板设为 MathTex 的默认模板
        # ...

# 使用示例
chinese_formula = MathTex(r"\text{能量 } E = mc^2", tex_template=cjk_template) # 也可以为单个对象指定模板
# 如果已经设置了默认模板,则无需再次指定:
# default_chinese_formula = MathTex(r"\text{动能 } K_R = \frac{1}{2} I \omega^2")
  • tex_compiler:

    • "latex": 传统的 LaTeX 编译器。
    • "pdflatex": 生成 PDF 的 LaTeX 编译器。
    • "xelatex": 对 Unicode 和现代字体 (OpenType, TrueType) 支持良好,是处理中文、日文、韩文等非拉丁字符的首选。
    • "lualatex": 另一个现代 LaTeX 编译器,功能与 xelatex 类似。
  • output_format:

    • ".dvi": latex 的输出。
    • ".pdf": pdflatex 的输出。
    • ".xdv": xelatex 的输出。Manim 之后会将其转换为 SVG。
  • preamble: LaTeX 文档的导言区。

    • \usepackage{...}: 加载 LaTeX 宏包。
      • amsmath, amssymb: 数学公式常用。
      • fontspec: xelatex 或 lualatex 用来加载系统字体。
      • xeCJK: xelatex 用来处理 CJK 字符的宏包。
    • \setCJKmainfont{字体名称}: 使用 xeCJK 设置 CJK 字符的字体。你需要确保你的系统安装了该字体,并且 LaTeX 能够找到它。
    • \setCJKsansfont{...}, \setCJKmonofont{...}: 可以分别设置无衬线和等宽 CJK 字体。
    • rf"""...""": Python 的原始多行字符串,非常适合包含 LaTeX 代码,因为反斜杠 \ 不需要额外转义。
  • 应用模板:

    • MathTex.set_default(tex_template=my_template): 将模板设置为所有 MathTex 对象的默认模板。
    • Tex.set_default(tex_template=my_template): 设置 Tex 对象的默认模板。
    • mobj = MathTex("...", tex_template=my_template): 为单个 Mobject 指定模板,会覆盖默认设置。

6.4 manim_utils.py 的作用

在您提供的脚本中,经常出现 from manim_utils import ...。这个 manim_utils.py 文件(未提供)很可能包含了项目中复用的一些工具函数和类,例如:

  • get_available_font(): 获取系统可用字体。
  • custom_voiceover_tts(): 处理文本到语音及字幕生成。
  • Layout, LayoutAtom, LayoutDirection: 自定义布局系统。
  • Title: 可能是一个预设样式的 Text 或 MathTex 子类。

这是一种良好的编程实践,将通用的辅助功能模块化,使得主场景脚本更专注于动画逻辑本身。

本章小结

  • 通过定义颜色常量,可以方便地管理和应用自定义颜色方案,增强动画的视觉风格和一致性。
  • Text 对象的字体可以通过 font 参数或 Text.set_default(font=...) 设置。
  • 对于需要 LaTeX 渲染的 MathTex 和 Tex 对象,尤其是涉及 CJK 字符时,应使用 TexTemplate 进行配置。
    • 推荐使用 xelatex 编译器和 xeCJK 宏包处理中文字体。
    • 确保在 preamble 中正确设置 CJK 字体。
  • 将常用的辅助函数和类放到如 manim_utils.py 这样的模块中,有助于代码的组织和复用。

下一章,我们将通过几个具体的案例分析,看看这些知识点是如何在实际的 Manim 项目中综合运用的。


第七章:实战演练——案例分析

现在,我们已经学习了 Manim 的基础知识,包括场景、Mobject、动画、坐标系、布局和自定义。接下来,我们将深入分析您提供的几个脚本,看看这些概念是如何在实际项目中应用的。

7.1 案例一:求解三角形角度 (源自 29_find_angles_of_triangles.txt)

这个脚本的目的是求解一个三角形的内角,已知三内角之比为 2:3:4。它清晰地展示了问题陈述、方程建立、求解过程和结果可视化的步骤。

核心知识点回顾:

  • 场景分段 (play_scene_01 到 play_scene_04)。
  • 自定义 Layout 类进行屏幕布局。
  • Text 和 MathTex 用于显示问题、公式和解题步骤。
  • 基本形状 Triangle, Dot, Angle。
  • VGroup 组织元素。
  • 动画 Write, Create, FadeOut, TransformMatchingTex (虽然此脚本中未使用,但类似场景常用)。
  • 自定义颜色。

代码片段分析:

  1. 场景搭建与问题陈述 (play_scene_01)

    # class CombinedScene(Scene):
    #     def play_scene_01(self):
    #         self.update_scene_number("01")
    #         layout = Layout(LayoutDirection.VERTICAL, { # 定义布局
    #             "title": (1.5, LayoutAtom()),
    #             "content": (7.0, Layout(LayoutDirection.HORIZONTAL, {
    #                 "problem": (4.0, LayoutAtom()),
    #                 "visual": (5.0, LayoutAtom())
    #             }))
    #         }).resolve(self)
    
    #         title = Text("Triangle Angle Problem", ...)
    #         layout["title"].place(title)
            
    #         problem = Text("In triangle ABC, the angles are in the ratio 2:3:4...", ...)
    #         layout["content"]["problem"].place(problem)
            
    #         triangle = Triangle(fill_opacity=0.2, color=self.triangle_color)
    #         # ... (添加 A, B, C 标签)
    #         triangle_group = VGroup(triangle, labels)
    #         layout["content"]["visual"].place(triangle_group)
            
    #         # ... (动画播放)
    #         self.title = title # 保存 Mobject 供后续使用
    #         self.problem = problem
    #         # ...
    
    • 清晰地使用 Layout 将屏幕分为标题区和内容区(内容区又分为问题文本区和视觉展示区)。
    • 创建 Text (问题) 和 Triangle (示意图)。
    • 使用 VGroup 组合三角形和其顶点标签。
    • 将创建的 Mobject 保存到 self 的属性中,以便在后续的 play_scene_XX 方法中可以引用或移除它们。
  2. 建立方程 (play_scene_02)

    # def play_scene_02(self):
    #     self.update_scene_number("02")
    #     # ... (布局)
    #     layout["title"].place(self.title) # 复用之前的标题对象
    
    #     eq_text = Text("Let's set up an equation...", ...)
    #     step1 = MathTex(r"\text{If we call the angles } \angle A, \angle B, \text{ and } \angle C", ...)
    #     # ... (step2 到 step7)
    #     steps = VGroup(step1, step2, step3, step4, step5, step6, step7).arrange(DOWN * 0.5, buff=0.4)
    #     layout["equations"].place(steps) # 假设布局中有 "equations" 区域
    
    #     # ... (动画)
    #     previous_objects = VGroup(self.problem, self.triangle, self.labels)
    #     self.play(FadeOut(previous_objects), run_time=1) # 淡出上一场景的元素
    #     self.play(Write(eq_text), run_time=1)
    #     self.play(Write(step1), run_time=1) # 逐步展示方程
    #     # ...
    
    • 复用了 self.title。
    • 使用 FadeOut 清理了上一个场景片段中不再需要的元素 (self.problem, self.triangle, self.labels)。
    • 用 MathTex 展示方程的推导步骤,并通过 VGroup 和 arrange 进行排版。
    • 通过一系列 Write 动画逐步呈现解题思路。
  3. 绘制精确角度的三角形 (play_scene_04) 这个片段比较复杂,因为它尝试根据计算出的角度 (40°, 60°, 80°) 来精确绘制三角形。

    # def play_scene_04(self):
    #     # ... (计算顶点坐标 vA, vB, vC 使用三角函数和边长)
    #     # base_length = 3
    #     # angle_A_rad = 40 * DEGREES
    #     # side_c = base_length
    #     # side_a = side_c * np.sin(angle_A_rad) / np.sin(angle_C_rad)
    #     # vB = np.array([0, 0, 0])
    #     # vA = np.array([side_c, 0, 0])
    #     # x_comp = side_b * np.cos(angle_C_rad) # 原代码这里似乎应该是 angle_A_rad 或 B
    #     # y_comp = side_b * np.sin(angle_C_rad) # 同上
    #     # vC = np.array([x_comp, y_comp, 0])
    #     # vertices = [vA, vB, vC]
    #     # final_triangle = Polygon(*vertices, ...)
    
    #     # 使用 Manim 的 Angle 类绘制角度弧
    #     # actual_vertices = final_triangle.get_vertices()
    #     # A, B, C = actual_vertices
    #     # angle_A_arc = Angle.from_three_points(C, A, B, radius=radius, ...)
    #     # angle_B_arc = Angle.from_three_points(A, B, C, radius=radius, ...)
    #     # angle_C_arc = Angle.from_three_points(B, C, A, radius=radius, ...)
    
    • 精确绘图的挑战:Manim 本身是基于坐标的。要绘制一个特定角度的三角形,你需要计算出顶点的坐标。这通常涉及到三角函数(正弦定理、余弦定理)。
    • Polygon(*vertices, ...): 通过顶点列表创建多边形。
    • Angle.from_three_points(P1, P2, P3, ...): P2是角的顶点,从P2-P1到P2-P3形成角。这个方法比手动创建 Arc 更方便。
    • 标签定位:角度标签的位置计算使用了角平分线的思路 (bisector_A = normalize(vec_AB + vec_AC)),这是一个很好的细节处理。

7.2 案例二:抛物线方程参数变化 (源自 25_Parabola_equation_function_graph_changes.txt)

这个脚本通过改变二次函数 y = ax^2 + bx + c 中的系数 a, b, c,动态展示抛物线形状和位置的变化。

核心知识点回顾:

  • Axes 创建坐标系。
  • axes.plot() 绘制函数图像。
  • ValueTracker 和 always_redraw 实现动态更新。
  • Transform 动画用于平滑改变图像和标签。
  • MathTex 显示公式和参数值。
  • 自定义 Layout。

代码片段分析:

  1. 参数 a 的影响 (play_section_01)

    # def play_section_01(self):
    #     # ... (布局, 标题, 坐标轴)
    #     label = MathTex("a=1", color=BLUE).next_to(axes.c2p(2, 4), UR, buff=0.1)
    #     graph = axes.plot(lambda x: x ** 2, color=BLUE, x_range=plot_x_range)
    #     self.play(Create(graph), Write(label), run_time=1.5)
    
    #     graph_a2 = axes.plot(lambda x: 2 * x ** 2,  x_range=plot_x_range)
    #     label_a2 = MathTex("a=2", color=GREEN).move_to(label) # 移动到原标签位置
    #     self.play(Transform(graph, graph_a2), Transform(label, label_a2), run_time=1.5)
        
    #     # ... (a=0.5, a=-1 的情况类似)
    
    • 为 a 的不同取值分别创建 axes.plot() 对象和对应的 MathTex 标签。
    • 使用 Transform 动画将前一个图形/标签平滑过渡到新的图形/标签。注意,Transform 会消耗第一个参数 Mobject,并将其替换为第二个参数 Mobject。
  2. 参数 b 的影响 (play_section_02) - 动态更新

    # def play_section_02(self):
    #     # ... (布局, 标题, 坐标轴)
    #     b_tracker = ValueTracker(0) # 初始 b=0
    #     a_val = 1
    #     c_val = 0
    
    #     # label 和 parabola 会自动重绘
    #     label = always_redraw(
    #         lambda: layout["body"]["graph_group"]["label"].place( # 假设 place 返回 Mobject
    #             MathTex(f"b = {b_tracker.get_value():.1f}", color=PURPLE)
    #         )
    #     )
    #     parabola = always_redraw(
    #         lambda: axes.plot(
    #             lambda x: a_val * x ** 2 + b_tracker.get_value() * x + c_val,
    #             color=PURPLE,
    #             x_range=plot_x_range
    #         )
    #     )
    #     self.add(parabola, label) # 添加到场景,开始自动更新
    
    #     self.play(b_tracker.animate.set_value(3), run_time=3.0) # b 从 0 变到 3
    #     self.wait(1.5)
    #     self.play(b_tracker.animate.set_value(-3), run_time=3.0) # b 从 3 变到 -3
    #     # ...
    
    • b_tracker = ValueTracker(0): 创建一个追踪参数 b 值的对象。
    • always_redraw(...): label 和 parabola 都是动态 Mobject。它们的 lambda 函数会在每一帧被调用:
      • label 的 lambda 函数根据 b_tracker.get_value() 创建新的 MathTex。
      • parabola 的 lambda 函数根据 b_tracker.get_value() 调用 axes.plot() 创建新的抛物线图像。
    • self.add(parabola, label): 将这些动态 Mobject 添加到场景后,它们就会开始根据 b_tracker 的值自动更新。
    • self.play(b_tracker.animate.set_value(3), ...): 动画地改变 b_tracker 的值。由于 label 和 parabola 依赖于 b_tracker,它们也会平滑地变化,形成动画效果。

7.3 案例三:光的折射 (源自 22_refraction_of_light.txt)

这个脚本用图示和公式解释了光的折射现象及斯涅尔定律。它展示了如何构建更复杂的几何图形和标签,并使用自定义颜色方案。

核心知识点回顾:

  • 自定义颜色常量。
  • Rectangle, Line, Arrow, Arc, DashedLine 构建图示。
  • MathTex 显示公式和希腊字母。
  • 三角函数 (np.sin, np.cos, np.radians, np.arcsin) 用于计算角度和位置。
  • rotate_vector (Manim 内置或自定义) 用于计算光线方向。
  • AnimationGroup 和 lag_ratio 控制动画序列。
  • Indicate 动画强调公式部分。

代码片段分析:

  1. 图示构建

    # def play_scene_01(self):
    #     # ... (布局, 背景, 标题)
    #     interface = Line(...) # 介质分界面
    #     air_rect = Rectangle(...) # 空气区域
    #     water_rect = Rectangle(...) # 水区域
    #     media_bg = VGroup(air_rect, water_rect).set_z_index(-5)
    
    #     # 光线计算 (涉及斯涅尔定律)
    #     incident_angle_deg = 45
    #     incident_angle_rad = np.radians(incident_angle_deg)
    #     n1 = 1.0; n2 = 1.33
    #     sin_theta2 = (n1 / n2) * np.sin(incident_angle_rad)
    #     refracted_angle_rad = np.arcsin(sin_theta2)
        
    #     incidence_point = np.array([diagram_center_x, interface_y, 0]) # 入射点
        
    #     # 使用 rotate_vector (假设是 Manim 的方法或自定义的)
    #     # incident_vector = rotate_vector(UP * 2.5, PI - incident_angle_rad)
    #     # ray_start = incidence_point + incident_vector
    #     # refracted_vector = rotate_vector(DOWN * 2.5, refracted_angle_rad)
    #     # ray_end = incidence_point + refracted_vector
    #     # 更常见的是直接用角度计算终点:
    #     ray_start = incidence_point + 2.5 * np.array([-np.sin(incident_angle_rad), np.cos(incident_angle_rad), 0])
    #     ray_end = incidence_point + 2.5 * np.array([np.sin(refracted_angle_rad), -np.cos(refracted_angle_rad), 0])
    
    
    #     incident_ray = Arrow(ray_start, incidence_point, ...)
    #     refracted_ray = Arrow(incidence_point, ray_end, ...)
    
    #     normal = DashedLine(...) # 法线
    #     angle_inc_arc = Arc(radius=0.8, start_angle=normal.get_angle() - PI / 2, angle=angle_inc_rad, ...)
    #     theta1_label = MathTex(r"\theta_1", ...).move_to(temp_arc1.point_from_proportion(0.5))
    #     # ... (其他标签和元素)
    
    • 大量使用基本形状来构建视觉场景。
    • np.radians() 将角度转为弧度,np.sin(), np.cos(), np.arcsin() 进行三角计算,这在物理和几何可视化中非常常见。
    • 光线的起点和终点是根据角度和长度计算出来的。
    • Arc 用于表示角度,其 start_angle 和 angle 参数需要仔细设置。arc.point_from_proportion(0.5) 获取弧中点,用于放置角度标签。
  2. 斯涅尔定律公式

    # snell_law_formula = MathTex(
    #     r"n_1 \sin \theta_1", r"=", r"n_2 \sin \theta_2", # 分割字符串以便分别上色
    #     font_size=48, color=MY_FORMULA_BLUE
    # )
    # snell_law_formula.set_color_by_tex("n_1", MY_DARK_TEXT)
    # snell_law_formula.set_color_by_tex(r"\theta_1", MY_HIGHLIGHT_RED)
    # # ... (其他部分上色)
    # self.play(Write(snell_law_formula))
    # self.play(Indicate(snell_law_formula.get_part_by_tex("n_1"), ...)) # 强调公式的 n1 部分
    
    • MathTex 的一个有用特性是,如果将 LaTeX 字符串分割成多个子字符串传递给它,之后可以用 mobject.set_color_by_tex("子字符串", color) 或 mobject.get_part_by_tex("子字符串") 来单独操作公式的特定部分。这对于教学中分步解释公式或高亮特定项非常有用。

7.4 案例四:二叉树 (源自 30_binary_tree.txt)

这个脚本展示了如何用 Manim 可视化二叉树数据结构,包括其基本结构、术语、不同类型的树以及遍历。

核心知识点回顾:

  • 自定义 Python 类 (BinaryTreeNode) 辅助创建和管理树的逻辑。
  • 递归或迭代思想构建树的可视化对象。
  • Circle 和 Text 组合成节点。
  • Line 连接节点形成边。
  • VGroup 组织整个树。
  • 复杂的布局和定位计算。

代码片段分析:

  1. BinaryTreeNode 辅助类

    # class BinaryTreeNode:
    #     def __init__(self, value, position=ORIGIN, level=0):
    #         self.value = value
    #         self.position = position # 节点在屏幕上的位置
    #         self.left = None
    #         self.right = None
    #         self.level = level
            
    #     def create_circle(self, radius=0.4, color="#1E88E5", text_color="#FFFFFF", ...):
    #         circle = Circle(radius=radius, color=color, fill_opacity=0.8, fill_color=color)
    #         text = Text(str(self.value), color=text_color, ...)
    #         text.move_to(circle.get_center())
    #         return VGroup(circle, text).move_to(self.position)
    
    • 这个辅助类并不直接是 Manim Mobject,但它存储了树节点的数据(值、子节点)以及其在 Manim 场景中的位置 (self.position) 和层级 (self.level)。
    • create_circle() 方法负责生成该节点对应的 Manim Mobject (一个包含 Circle 和 Text 的 VGroup)。
  2. 创建树的可视化 (create_tree_visualization 方法)

    # def create_tree_visualization(self, root_value=1, depth=3, ...):
    #     root = BinaryTreeNode(root_value) # 根节点,位置默认 ORIGIN
    #     nodes_queue = [(root, 0, 0)]  # (node_obj, horizontal_screen_pos_offset, level)
    #     all_nodes_data = [] # 存储 BinaryTreeNode 对象
    #     edges_coords = [] # 存储边的 (起点坐标, 终点坐标)
        
    #     while nodes_queue:
    #         node_obj, h_pos_offset, level = nodes_queue.pop(0)
    #         node_obj.position = np.array([h_pos_offset, -self.level_height * level, 0]) # 计算实际屏幕位置
            
    #         if level >= depth: continue
                
    #         # 计算子节点的位置 (相对父节点进行偏移)
    #         # h_spacing_factor = self.horizontal_spacing / (2 ** (level + 1)) # 水平间距随层级减小
    #         # left_child_h_offset = h_pos_offset - h_spacing_factor
    #         # right_child_h_offset = h_pos_offset + h_spacing_factor
    
    #         # ... (创建左子节点 BinaryTreeNode 对象)
    #         # node_obj.left = BinaryTreeNode(...)
    #         # nodes_queue.append((node_obj.left, left_child_h_offset, level + 1))
    #         # edges_coords.append((node_obj.position, node_obj.left.position)) # 错误:此时子节点 position 未定
    #         # all_nodes_data.append(node_obj.left)
    #         # ... (右子节点类似)
    
    #     # 修正:应在子节点 position 确定后再记录 edge
    #     # 应该在循环外,或在子节点 position 计算后,重新遍历收集 edges
    #     # 或者,在创建子节点时,其 position 已经基于父节点和偏移计算好了,可以直接用
    
    #     # 创建 Manim Mobjects
    #     node_mobjects = VGroup()
    #     node_mobjects.add(root.create_circle(...)) # 创建根节点的 Mobject
    #     for node_data in all_nodes_data: # all_nodes_data 应包含除 root 外的其他 BinaryTreeNode 对象
    #         node_mobjects.add(node_data.create_circle(...))
            
    #     edge_mobjects = VGroup()
    #     # 再次遍历或使用已存储的正确坐标创建 Line 对象
    #     # for start_pos, end_pos in edges_coords:
    #     #    edge_mobjects.add(Line(start_pos, end_pos, ...))
            
    #     tree_viz = VGroup(edge_mobjects, node_mobjects)
    #     tree_viz.move_to(shift_vector) # 整体移动
    #     return tree_viz, root_data_obj, all_nodes_data_objs
    
    • 逻辑与可视化的分离:首先使用 BinaryTreeNode 构建树的逻辑结构并计算每个节点在屏幕上的目标位置。这个计算过程通常是递归的或基于层序遍历的。
    • 位置计算:节点的 y 坐标通常取决于其层级 (-self.level_height * level)。x 坐标则更复杂,需要确保子节点在父节点下方且互不重叠,通常子节点间的水平间距会随着层级的增加而减小。
    • Mobject 生成:在所有节点的位置都计算完毕后,遍历 BinaryTreeNode 对象列表,为每个对象调用其 create_circle() 方法生成对应的 Manim Mobject。同时,根据父子关系和它们的位置创建 Line Mobject作为边。
    • 整体组合:最后将所有的节点 Mobject 和边 Mobject 添加到一个大的 VGroup 中,形成完整的树的可视化。

这个案例展示了当内置 Mobject 不足以直接表达复杂结构时,如何通过自定义辅助类和精心的位置计算来构建自定义的可视化。

本章小结

通过分析这些案例,我们可以看到:

  • 规划是关键:在编写代码之前,先构思动画的步骤和最终效果。
  • 分解问题:将复杂的动画分解为小的、可管理的片段或方法。
  • 善用工具:VGroup, Layout (自定义), ValueTracker 等工具能极大提高效率。
  • 数学计算:对于几何和物理相关的可视化,精确的数学计算(三角函数、向量运算等)是必不可少的。
  • 代码复用:将通用的功能(如场景编号更新、布局)封装成函数或类。
  • 细节处理:如标签的精确定位、颜色的搭配、动画的节奏感,共同决定了最终动画的质量。

这仅仅是 Manim 能力的冰山一角。希望通过这些案例的分析,您对如何使用 Manim 构建自己的数学动画有了更具体的认识。


第八章:画龙点睛——语音与字幕

虽然 Manim 本身专注于视觉动画的生成,但在许多科普视频中,同步的语音解说和字幕是不可或缺的。您提供的脚本中使用了 custom_voiceover_tts 这个自定义工具来集成语音和(推测)字幕。

8.1 custom_voiceover_tts 的工作模式推测

从脚本中的使用方式来看,custom_voiceover_tts 可能是一个上下文管理器 (context manager),它做了以下几件事情:

  1. 接收文本:with custom_voiceover_tts(voice_text_string) as tracker:
  2. 文本转语音 (TTS):在后台调用某个 TTS 引擎(如 gTTS, Azure TTS, Edge TTS 等)将 voice_text_string 转换为音频文件。
  3. 获取音频信息:tracker 对象可能包含转换后的音频文件路径 (tracker.audio_path) 和音频时长 (tracker.duration)。
  4. (可选)生成字幕:可能还会根据文本和时长信息生成 .srt 或类似格式的字幕文件。
  5. 同步:允许在 with 代码块内部的 Manim 动画与语音时长进行同步。
# 伪代码,演示其核心用法
# from manim_utils import custom_voiceover_tts # 假设的导入

# voice_text = "这是一段解说词。"
# with custom_voiceover_tts(voice_text) as tracker:
#     # tracker.audio_path 存放生成的音频文件路径
#     # tracker.duration 存放音频时长(秒)

#     if tracker.audio_path and tracker.duration > 0:
#         self.add_sound(tracker.audio_path) # Manim 的方法,在视频中添加音轨

#     # --- Manim 动画代码 ---
#     animation_start_time = self.renderer.time # 获取当前渲染时间 (Manim CE)
#                                               # 或者直接计算动画时长
#     self.play(Write(some_text), run_time=2)
#     self.play(Create(some_shape), run_time=1.5)
#     # --- 结束 Manim 动画代码 ---

#     # 计算动画总时长
#     # anim_duration = self.renderer.time - animation_start_time
#     anim_duration = 2 + 1.5

#     # 计算需要额外等待的时间以匹配语音
#     wait_time = 0
#     if tracker.duration > 0: # 确保语音已生成
#         wait_time = max(0.001, tracker.duration - anim_duration)
#     else: # 如果语音失败,可以设置一个默认等待时间
#         wait_time = 0.5

#     self.wait(wait_time)

8.2 Manim 的 self.add_sound()

Manim 场景类本身提供了一个 self.add_sound(audio_file_path, time_offset=0, gain=None) 方法。

  • audio_file_path: 要添加的音频文件的路径。
  • time_offset: 音频相对于场景开始播放的偏移时间(秒)。默认是0,即从当前 self.play 或 self.wait 开始时播放。在 with 块中使用时,通常意味着音频与该块内的动画同步开始。
  • gain: 音量增益。

当 Manim 渲染视频时,它会使用 FFmpeg 将这些指定的音轨混入最终的视频文件中。

8.3 字幕的集成

Manim 本身不直接生成或渲染字幕到视频帧上(除非你手动创建 Text Mobject 作为字幕)。custom_voiceover_tts 工具如果生成了 .srt 文件,那么这个字幕文件可以:

  1. 在视频编辑软件中导入,与最终视频合并。
  2. 上传到视频平台(如 YouTube, Bilibili)时,作为独立的字幕文件上传。

8.4 如果没有 custom_voiceover_tts 怎么办?

如果你的项目中没有这个特定的工具,你仍然可以实现类似的功能:

  1. 手动录制或生成语音:使用任何你喜欢的 TTS 工具(如 Microsoft Edge 浏览器的朗读功能、在线 TTS 服务、Audacity 等录音软件)为你的解说词生成音频文件 (如 .mp3, .wav)。
  2. 获取音频时长:使用音频编辑软件或工具(如 ffprobe, Python 的 mutagen 库)获取每个音频文件的准确时长。
  3. 在 Manim 中同步:
    # 假设你已经为 scene_part1_voice.mp3 测得时长为 5.2 秒
    voice1_duration = 5.2
    self.add_sound("path/to/scene_part1_voice.mp3")
    
    anim1_duration = 0
    self.play(Write(text1), run_time=2); anim1_duration += 2
    self.play(Create(shape1), run_time=1.5); anim1_duration += 1.5
    # ... 其他动画 ...
    
    wait_time = max(0, voice1_duration - anim1_duration)
    self.wait(wait_time)
    
  4. 手动创建字幕文件:根据你的解说词和对应的时间轴,手动编写 .srt 文件。一个 .srt 条目的格式如下:
    1
    00:00:05,000 --> 00:00:08,500
    这是第一句字幕,从第5秒显示到第8.5秒。
    
    2
    00:00:09,200 --> 00:00:12,000
    这是第二句字幕。
    

8.5 语音和动画节奏的重要性

无论是使用自动化工具还是手动操作,确保语音解说和屏幕上的视觉动画节奏合拍至关重要。

  • 动画不宜过快,以免观众跟不上解说。
  • 动画也不宜过慢,以免语音结束后长时间等待。
  • 重要的视觉变化应该与解说中提到它的时间点大致吻合。

这通常需要反复调整动画的 run_time 和 self.wait() 的时长。

本章小结

  • 通过 self.add_sound() 方法,Manim 可以在最终渲染的视频中加入音轨。
  • 自定义工具如 custom_voiceover_tts 可以自动化文本到语音的转换、音频时长的获取以及与 Manim 动画的同步过程。
  • 即使没有自动化工具,也可以通过手动生成语音、测量时长并在 Manim 中精心安排 self.wait() 来实现同步。
  • 字幕文件(如 .srt)通常是独立于 Manim 生成的,之后与视频合并或在视频平台上传。
  • 保持语音和动画的节奏协调对于提升观看体验非常重要。

第九章:融会贯通——更复杂的案例剖析

在前面的章节中,我们已经分别探讨了 Manim 的核心组件和常用技巧。现在,让我们来看一些更复杂的脚本,观察这些知识点是如何综合运用以构建出信息丰富且视觉吸引的动画的。

9.1 案例一:欧拉公式的几何意义 (源自 21_Geometric_Meaning_of_Euler_s_Formula.txt)

这个脚本旨在解释欧拉公式 (e^{i\theta} = \cos\theta + i\sin\theta) 的几何意义,涉及到复平面、单位圆、三角函数以及动态可视化。

关键技术点:

  • 场景分段与清理:使用 play_section_XX 方法组织内容,并通过 clear_section 管理元素,允许在不同部分之间保留核心元素(如坐标轴、单位圆)。
    # self.play_section_02() # 引入复平面
    # self.clear_section(keep_refs=['axes', 'axes_labels']) # 清理,但保留坐标轴
    # self.play_section_03() # 单位圆上的点 cos + i sin
    # self.clear_section(keep_refs=['axes', 'axes_labels', 'unit_circle', ...]) # 保留更多
    
    keep_refs 参数允许指定哪些 self 的属性(即 Mobject)不被 clear_section 移除。
  • 复平面与单位圆:
    • 使用 Axes 创建复平面,并手动添加 "Re" 和 "Im" 标签。
    • 根据 Axes 的单位尺寸计算并创建 Circle 作为单位圆。
    • 使用 ValueTracker (theta_tracker) 和 add_updater (或 always_redraw) 使单位圆上的点 point_P、半径 radius_line、角度弧 angle_arc、角度值文本 theta_value_text 以及点标签 e_label 动态地随着角度 theta_tracker 的变化而更新。
    # theta_tracker = ValueTracker(0)
    # self.point_P = Dot(color=BLUE, radius=0.08)
    # self.point_P.add_updater(
    #     lambda m: m.move_to(self.axes.c2p(np.cos(theta_tracker.get_value()), np.sin(theta_tracker.get_value())))
    # )
    # # ... 其他 updaters ...
    # self.play(theta_tracker.animate.set_value(TAU), run_time=animation_duration, rate_func=linear)
    
  • TracedPath: trace = TracedPath(self.point_P.get_center, ...) 用于追踪 point_P 的运动轨迹,在动态展示点P绕单位圆运动时,会画出圆的轨迹。
  • 公式的逐步呈现与变换:
    • formula = MathTex(r"e^{i\theta}", r"=", r"\cos\theta", r"+", r"i\sin\theta") 将公式拆分为多个部分,便于分步 Write 或分别设置颜色。
    • ReplacementTransform(self.p_label_complex, self.e_label) 将标签从 \cos\theta + i\sin\theta 平滑地变成 e^{i\theta},强调两者的等价性。

这个案例的亮点:

  • 动态关联:通过 ValueTracker 和 updater 将数学参数(角度 θ)与视觉元素(点的位置、线的方向、弧的大小)紧密联系起来,实现了真正意义上的动态数学可视化。
  • 概念递进:从复平面基础,到单位圆上的三角表示,再到欧拉公式的引入和动态演示,逻辑清晰,层层深入。
  • 元素管理:在多阶段场景中,通过 clear_section 和 keep_refs 有效地管理了元素的生命周期,避免了屏幕混乱,同时保留了必要的上下文。

9.2 案例二:天空为什么是蓝色的 (源自 24_why_sky_is_blue.txt)

这个脚本解释了瑞利散射导致天空呈现蓝色的物理现象。它涉及太阳光、大气层、光的散射等多个概念,并使用了丰富的视觉元素。

关键技术点:

  • 自定义颜色方案:脚本开头定义了大量自定义颜色(MY_SUN_YELLOW, MY_SKY_BLUE, MY_ATMOSPHERE_BLUE 等),用于统一视觉风格。
  • 多层视觉元素:
    • 太阳 (Circle)、棱镜 (Triangle)、光谱 (VGroup of Lines) 来解释白光分解。
    • 地球 (Circle + Polygons for continents)、大气层 (Annulus)、大量分子 (VGroup of Dots) 来模拟大气环境。
    • 入射光线 (Arrow) 与分子交互后产生散射光线 (多个 Arrows)。
  • 随机性与模拟:
    • 大气中分子的位置 (np.random.uniform) 和散射光线的方向 (np.random.uniform) 引入了随机性,使模拟更自然。
    # molecules = VGroup()
    # for _ in range(50): # 创建50个分子
    #     angle = np.random.uniform(0, TAU)
    #     radius = np.random.uniform(2.6, 2.9) # 在大气层环内随机半径
    #     pos = atmosphere.get_center() + radius * (np.cos(angle)*RIGHT + np.sin(angle)*UP)
    #     mol = Dot(pos, radius=0.03, color=MY_MOLECULE_COLOR)
    #     molecules.add(mol)
    
  • 概念图示分离:
    • “瑞利散射”部分 (play_section_04):用一个中心分子和不同颜色的散射光线(蓝/紫光线多且散射角度大,红/橙光线少且主要向前)来形象地解释散射与波长的关系。
    • “为什么我们看到蓝色”部分 (play_section_05):用地面观察者和天空中大量蓝色散射点源来到达观察者眼睛的示意图。
  • 解释“为什么不是紫色”:通过并列展示简化的太阳光谱能量分布图和人眼敏感度曲线图(用 Axes 和 plot 构建)来解释这一常见疑问。

这个案例的亮点:

  • 复杂现象的简化表达:将复杂的物理过程(光的组成、大气成分、散射原理、人眼感知)分解为一系列可理解的视觉步骤。
  • 视觉元素的巧妙运用:例如用 Annulus 表示大气层,用大量随机 Dot 表示分子,用不同颜色和数量的箭头表示不同波长光的散射情况。
  • 多角度解释:不仅解释了为什么是蓝色,还进一步解释了为什么不是散射更强的紫色,使解释更完整。
  • 信息密度与节奏控制:在有限的时间内呈现了大量信息,并通过分段和语音同步(假设 custom_voiceover_tts 正常工作)来控制节奏。

9.3 案例三:转动动能与角动量 (源自 31_rotational_kinetic_energy_and_angular_momentum.txt)

这是一个物理问题求解的脚本,计算飞轮的转动动能。它侧重于公式的展示、单位换算和代入计算。

关键技术点:

  • 中文字体与 TexTemplate:如第六章所述,正确配置了 cjk_template 以支持 MathTex 中的中文文本和公式。
  • 自定义 Layout:使用了 Layout 类将屏幕划分为标题区和左右内容区,用于清晰展示公式、已知条件和计算步骤。
  • MathTex 的精细使用:
    • 公式、单位、数值计算都使用 MathTex,排版美观。
    • TransformMatchingTex 用于在公式推导的不同步骤之间平滑过渡,例如从角速度单位换算公式到代入数值,再到计算结果。
    # omega_conversion_formula = MathTex(r"\omega (\text{rad/s}) = \omega (\text{rpm}) \times \frac{2\pi ...}{...}", ...)
    # omega_conversion_calc = MathTex(r"= 1200 \times \frac{2\pi}{60} ...", ...)
    # omega_converted_value = MathTex(r"= 40\pi \, \text{rad/s}", ..., color=self.highlight_color)
    
    # self.play(Write(omega_conversion_formula))
    # self.play(TransformMatchingTex(omega_conversion_formula.copy(), omega_conversion_calc))
    # self.play(TransformMatchingTex(omega_conversion_calc.copy(), omega_converted_value))
    
    注意这里 .copy() 的使用,因为 TransformMatchingTex 会消耗第一个参数,如果后续还需要基于它进行变换或它本身就是动态更新的一部分,就需要复制。
  • 逐步展示 (Staged Animation):计算过程不是一次性全部显示,而是通过多个 self.play 调用,结合 Write, FadeIn, TransformMatchingTex 逐步呈现,符合教学的逻辑。
  • 高亮:使用 self.highlight_color (如 PURE_RED) 突出显示重要的中间结果或最终答案。

这个案例的亮点:

  • 教学流程的模拟:动画的组织方式非常贴近一个老师在黑板上演算物理题的过程:给出公式 -> 列出已知 -> 单位换算 -> 代入计算 -> 得出结果 -> 近似计算。
  • TransformMatchingTex 的有效应用:使得公式的演变过程非常自然和连贯,帮助观众理解每一步的联系。
  • 排版清晰:通过 Layout 和 VGroup.arrange,即使公式和步骤较多,屏幕依然保持整洁有序。

通用启示:

  • 模块化与复用:update_scene_number, Layout, custom_voiceover_tts 这些自定义工具在多个脚本中出现,体现了良好编程实践中的模块化和代码复用。
  • 注释与可读性:虽然这里分析的是最终代码,但在开发复杂场景时,良好的注释和有意义的变量命名至关重要。
  • 迭代与调试:创建复杂的 Manim 动画往往是一个迭代的过程。使用低质量快速渲染 (-ql) 进行调试,逐步完善细节。

通过这些案例,我们可以看到,掌握了 Manim 的基本构建块之后,创造复杂而精美的数学或科学动画就变成了如何巧妙地组合这些构建块,并用清晰的逻辑来组织动画流程的问题。


第十章:进阶之路与学习资源

恭喜你!通过前面的章节和案例分析,你已经对如何使用 Manim 编写代码来创作数学动画有了坚实的基础。Manim 是一个非常强大的工具,它的潜力远不止于此。本章将为你指明一些进阶方向和有用的学习资源。

10.1 探索更多 Manim 功能

  • 3D 动画 (ThreeDScene): Manim 不仅限于2D,它也支持3D场景、3D图形 (Sphere, Cube, ParametricSurface 等) 和3D相机操作。
    class My3DScene(ThreeDScene):
        def construct(self):
            self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES)
            axes = ThreeDAxes()
            sphere = Sphere()
            self.add(axes, sphere)
            self.begin_ambient_camera_rotation(rate=0.5) # 自动旋转相机
            self.wait(5)
    
  • SVG 图像 (SVGMobject): 你可以导入 SVG 文件并在 Manim 中对其进行动画处理。这对于使用矢量图形软件(如 Inkscape, Illustrator)设计的复杂图形非常有用。
  • 自定义 Mobject: 当内置图形不满足需求时,可以继承 Mobject 或 VMobject (Vectorized Mobject) 来创建自己的图形类,完全控制其形状和行为。30_binary_tree.txt 中的 BinaryTreeNode 虽然不是直接的 Mobject,但其 create_circle 方法生成了 Mobject,这种封装思想可以扩展到创建自定义 Mobject。
  • 更高级的动画:
    • LaggedStart: 类似 AnimationGroup 但提供更细致的动画启动延迟控制。
    • Succession: 按顺序播放一系列动画。
    • 自定义动画:通过继承 Animation 类并重写 interpolate_mobject 方法,你可以创建全新的动画效果。
  • 交互性 (ManimGL): 如果你使用的是 ManimGL (Grant Sanderson 最初使用的 OpenGL 版本,与社区版 ManimCE 不同),它支持实时渲染和一些基本的交互功能。但目前社区版 ManimCE 更侧重于高质量的视频输出。
  • 着色器 (Shaders): 对于非常高级的视觉效果,可以利用着色器编程(GLSL),但这需要图形学背景。

10.2 提升代码质量与效率

  • 面向对象编程 (OOP): 更深入地运用类和对象来组织你的 Mobject 和动画逻辑,特别是对于大型项目。
  • 代码复用: 将常用的 Mobject 组合、动画序列或辅助函数封装到单独的 Python 模块中(就像 manim_utils.py 那样)。
  • 配置文件: Manim 支持通过配置文件 (manim.cfg) 设置默认参数,如输出目录、背景颜色、帧率等,避免在每个脚本中重复设置。
  • 版本控制 (Git): 对于任何编程项目,使用 Git 进行版本控制都是一个好习惯,便于追踪修改、协作和回滚。

10.3 学习资源

  • Manim 官方文档:
    • Manim Community Edition (ManimCE): https://docs.manim.community/ 这是目前最活跃和推荐的版本,拥有详尽的文档和示例。
  • 示例库:
    • 官方文档中包含大量示例。
    • GitHub 上搜索 "Manim examples" 可以找到许多用户分享的项目。
  • 社区:
    • Manim Community Discord 服务器: https://www.manim.community/discord/ 非常活跃的社区,可以提问、分享作品、获取帮助。
    • Reddit r/manim: https://www.reddit.com/r/manim/
  • 教程视频:
    • YouTube 上有许多 Manim 教程,搜索 "Manim tutorial" 即可。一些知名的 Manim 内容创作者(除了 3Blue1Brown)也会分享他们的制作经验。
    • Bilibili 上也有不少中文 Manim 教程和作品分享。
  • 阅读优秀代码:
    • 研究 3Blue1Brown 的旧视频代码 (manim-old 或 manim_sandbox GitHub 仓库,主要是 ManimGL)。
    • 查看 Manim Community 维护者和其他贡献者的代码。
    • 您提供的这些脚本本身就是很好的学习材料,尝试修改它们,实现新的效果。

10.4 实践,实践,再实践!

学习编程和使用像 Manim 这样的库,最重要的方法就是动手实践。

  • 从小处着手: 尝试修改现有示例,改变颜色、文本、动画时长。
  • 设定目标: 选择一个你感兴趣的数学或科学概念,尝试用 Manim 将其可视化。
  • 分解问题: 将复杂的动画目标分解为小的、可实现的任务。
  • 不怕犯错: 调试是学习过程的一部分。仔细阅读错误信息,利用社区资源寻求帮助。
  • 分享你的作品: 将你的动画分享出去,获取反馈,也能激励自己继续学习。

结语

Manim 是一个充满创造力的工具,它将编程的严谨与艺术的表达完美结合。通过本书的学习,你已经迈出了用代码创造数学之美的第一步。前方的道路充满挑战,但也同样充满乐趣和成就感。

希望您提供的这些脚本和本书的讲解能为您打下坚实的基础。不断探索,不断创造,用 Manim 将抽象的概念变得生动直观!

祝您在 Manim 的世界里玩得开心!


Edit this page
Last Updated:
Contributors: Tong Li
Prev
成本核算