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 打包成 FatJar
    • 使用 GraalVM 构建 tio-boot Native 程序
    • 使用 Docker 部署 tio-boot
    • 部署到 Fly.io
    • 部署到 AWS Lambda
    • 到阿里云云函数
    • 使用 Deploy 工具部署
    • 使用Systemctl启动项目
    • 使用 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
    • 请求拦截器
    • Controller拦截器
    • LoggingInterceptor
    • 全局异常处理器
    • 异步处理
    • 动态 返回 CSS 实现
    • 返回图片
    • Transfer-Encoding: chunked 实时音频播放
    • Server-Sent Events (SSE)
    • 接口访问统计
    • 接口请求和响应数据记录
    • 自定义 Handler 转发请求
    • 使用 HttpForwardHandler 转发所有请求
    • 跨域
    • 添加 Controller
    • 常用工具类
    • HTTP Basic 认证
    • Http响应加密
    • 在 Tio-boot 中使用零拷贝发送大文件
    • Tio Boot 分片上传服务设计与实现
    • WebJars
    • JProtobuf
    • Tio-Boot HTTP Speed Test
  • 07_validate

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

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

    • java‑db
    • 操作数据库入门示例
    • SQL 模板 (SqlTemplates)
    • 数据源配置与使用
    • ActiveRecord
    • Db 工具类
    • 批量操作
    • Model
    • 生成器与 Model
    • 注解
    • 异常处理
    • 数据库事务处理
    • Cache 缓存
    • Dialect 多数据库支持
    • 表关联操作
    • 复合主键
    • Oracle 支持
    • Enjoy SQL 模板
    • 整合 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 导入
    • 预留
    • 预留
    • ApiTable 实现增删改查
    • 数组类型
    • 单独使用 ApiTable
    • TQL(Table SQL)前端输入规范
  • 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_认证和权限

    • FixedTokenInterceptor
    • TokenManager
    • 数据表
    • 匿名登录
    • 注册和登录
    • 个人中心
    • 重置密码
    • Google 登录
    • 短信登录
    • 移动端微信登录
    • 移动端重置密码
    • 微信登录
    • 移动端微信登录
    • 权限校验注解
    • Sa-Token
    • sa-token 登录注册
    • StpUtil.isLogin() 源码解析
  • 14_i18n

    • i18n
  • 15_enjoy

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

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

    • TioBootTest 类
  • 18_tio

    • TioBootServer
    • 使用 tio-core 在 tio-boot 中构建独立的 TCP 服务器
    • 内置 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
    • Email
    • JSON
    • File
    • 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
    • TCP数据转发
  • 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
    • 存储文件到 阿里云 OSS
  • 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-kit-server

    • Java 执行 python 代码
    • 通过大模型执行 Python 代码
    • GeoGebra
    • /zh/65_java-kit-server/04.html
    • /zh/65_java-kit-server/05.html
    • /zh/65_java-kit-server/06.html
    • 视频下载增加水印说明文档
    • MCP 协议
    • Cline 提示词
    • Cline 提示词-中文版本
    • /zh/65_java-kit-server/11.html
  • 66_manim

    • Teach me anything - 基于大语言的知识点讲解视频生成系统
    • 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/32.html
    • /zh/66_manim/33.html
  • 68_java-llm-proxy

    • 使用tio-boot搭建多模型LLM代理服务
  • 69_ai-brower

    • AI Browser:基于用户指令的浏览器自动化系统
    • 提示词
    • dom构建- buildDomTree.js
    • dom构建- 将网页可点击元素提取与可视化
    • 提取网内容
    • 启动浏览器
    • 操作浏览器指令
  • 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 性能测试报告
  • 73_tio-mail-wing

    • tio-mail-wing简介
    • 任务1:实现POP3系统
    • 使用 getmail 验证 tio-mail-wing POP3 服务
    • 任务2:实现 SMTP 服务
    • 数据库初始化文档
    • 用户管理
    • 邮件管理
    • 任务3:实现 SMTP 服务 数据库版本
    • 任务4:实现 POP3 服务(数据库版本)
    • IMAP 协议
    • 拉取多封邮件
    • 任务5:实现 IMAP 服务(数据库版本)
    • IMAP实现讲解
    • IMAP 手动测试脚本
    • IMAP 认证机制
    • 主动推送
  • 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 命令

02

topic

用图形化方式证明7x5x2=7x(5x2)

场景提示

## 图形化证明:乘法结合律 \( 7 \times 5 \times 2 = 7 \times (5 \times 2) \)

### 【场景一:问题引入与标题展示】

*   **背景设计与整体氛围**
    *   背景采用从浅灰 (#DDDDDD) 到白色 (#FFFFFF) 的垂直渐变,营造简洁、清晰的学术氛围。
    *   右上角显示场景编号“01”,字体稍小,颜色为深灰色 (#555555)。

*   **主要内容**
    *   屏幕中心偏上位置出现标题:“图形化证明:乘法结合律”,文字颜色为黑色 (#000000),使用 `Write` 动画效果,时长 1.5 秒。
    *   标题下方展示核心待证明的数学公式:
        \[
        7 \times 5 \times 2 = 7 \times (5 \times 2)
        \]
    *   该公式使用 LaTeX 渲染,字体大小比标题稍小,颜色为深蓝 (#00008B)。采用 `FadeIn` 动画效果,在标题完全显示后延迟 0.5 秒出现,时长 1 秒。
    *   数字 7、5、2 可以使用不同颜色(例如:7-红色 #FF0000, 5-绿色 #008000, 2-蓝色 #0000FF)来突出它们在后续图形化过程中的对应关系,括号也需清晰显示。

*   **相机与过渡效果**
    *   相机保持静态,聚焦于屏幕中心内容。
    *   整体动画节奏清晰,标题和公式依次出现,留有足够的阅读时间。

---

### 【场景二:可视化计算 (7 x 5) x 2】

*   **背景与布局**
    *   背景保持浅灰到白色的渐变。
    *   屏幕布局分为左右两部分:
        *   左侧区域(约占屏幕 1/3)用于显示文字说明和计算步骤。
        *   右侧区域(约占屏幕 2/3)用于展示三维图形可视化。
    *   右上角显示场景编号“02”。
    *   在右侧区域设置三维坐标系 `ThreeDAxes`,x, y, z 轴有清晰标签,轴范围大致为 x: [0, 8], y: [0, 6], z: [0, 3]。

*   **内容呈现**
    *   **左侧文本与公式:**
        1.  首先出现文本:“计算方式一:先算 \(7 \times 5\)”,使用 `Write` 效果,时长 1 秒。
        2.  下方展示计算过程:
            \[
            (7 \times 5) \times 2
            \]
            该公式使用 LaTeX 渲染,其中 `(7 \times 5)` 部分高亮(例如,背景色设为淡黄色 #FFFFE0)。`FadeIn` 效果,时长 0.5 秒。
        3.  再下方显示中间结果:“\( 7 \times 5 = 35 \)” ,使用 `Write` 效果,时长 1 秒。
        4.  最后显示完整计算:“\( = 35 \times 2 = 70 \)” ,使用 `ReplacementTransform` 从 `(7 \times 5) \times 2` 平滑过渡,时长 1.5 秒。
    *   **右侧三维可视化:**
        1.  **构建 7x5 底面**:在 XY 平面上,生成一个由 7x5=35 个小立方体 (`Cube`) 组成的 VGroup,排列成 7 行 5 列。每个小立方体边长为 0.5。使用 `Create` 动画逐个或逐行/列生成,整体时长 2 秒。这组立方体颜色设为红色 (#FF0000),代表数字 7 的维度,和绿色 (#008000),代表数字 5 的维度,可以交错或用边框颜色区分。或者统一用一种颜色(如橙色 #FFA500)表示乘积 35。
        2.  **标注尺寸**:在对应的 X 轴和 Y 轴方向上添加标签 "7" 和 "5"。
        3.  **向上堆叠**:将这个 7x5 的底层复制一层,并向上移动一个单位(2 * 小立方体边长)的高度。第二层使用 `TransformFromCopy` 或 `Create` 效果生成,时长 1.5 秒。第二层立方体颜色使用蓝色 (#0000FF),代表乘以 2。
        4.  **标注高度**:在 Z 轴方向添加标签 "2"。
        5.  最终形成一个 7x5x2 的长方体。

*   **相机与动画效果**
    *   初始相机视角设置为 `phi=75*DEGREES, theta=-45*DEGREES`,能清晰看到 XY 平面和 Z 轴。
    *   在创建 7x5 底面时,相机可以轻微俯视。
    *   在向上堆叠时,相机可以稍微调整角度(例如,`phi` 减小一点,`theta` 变化一点)或轻微拉远,以更好地展示立体结构。使用 `self.move_camera(...)` 平滑过渡,时长与堆叠动画同步。
    *   左侧文本与右侧图形动画同步进行,例如,当显示 \(7 \times 5 = 35\) 时,右侧刚好完成 7x5 底面的构建。当显示 \( \times 2 \) 时,开始向上堆叠。

---

### 【场景三:可视化计算 7 x (5 x 2)】

*   **背景与布局**
    *   背景和布局与场景二类似:浅灰渐变背景,左文右图,三维坐标轴。
    *   右上角显示场景编号“03”。
    *   三维坐标轴设置与场景二相同 (x: [0, 8], y: [0, 6], z: [0, 3]),但初始构建的平面可能不同。

*   **内容呈现**
    *   **左侧文本与公式:**
        1.  首先出现文本:“计算方式二:先算 \(5 \times 2\)”,使用 `Write` 效果,时长 1 秒。
        2.  下方展示计算过程:
            \[
            7 \times (5 \times 2)
            \]
            该公式使用 LaTeX 渲染,其中 `(5 \times 2)` 部分高亮(淡黄色背景 #FFFFE0)。`FadeIn` 效果,时长 0.5 秒。
        3.  再下方显示中间结果:“\( 5 \times 2 = 10 \)” ,使用 `Write` 效果,时长 1 秒。
        4.  最后显示完整计算:“\( = 7 \times 10 = 70 \)” ,使用 `ReplacementTransform` 从 `7 \times (5 \times 2)` 平滑过渡,时长 1.5 秒。
    *   **右侧三维可视化:**
        1.  **构建 5x2 "侧面"**:例如,在 YZ 平面上(或 XY 平面上,但组织方式不同),生成一个由 5x2=10 个小立方体 (`Cube`) 组成的 VGroup,排列成 5 行 2 列。小立方体边长 0.5。使用 `Create` 动画,时长 1.5 秒。这组立方体颜色可以结合绿色 (#008000) 代表 5,蓝色 (#0000FF) 代表 2。或者统一用一种颜色(如青色 #00FFFF)表示乘积 10。
        2.  **标注尺寸**:在对应的 Y 轴和 Z 轴(或 X 和 Y 轴)方向上添加标签 "5" 和 "2"。
        3.  **沿另一方向扩展**:将这个 5x2 的“切片”沿着 X 轴(或 Z 轴)方向复制扩展 7 次(或移动 7 * 小立方体边长 的距离)。使用 `TransformFromCopy` 或 `Create` 效果逐层生成,时长 2 秒。新扩展的维度用红色 (#FF0000) 代表 7。
        4.  **标注长度**:在 X 轴(或 Z 轴)方向添加标签 "7"。
        5.  最终也形成一个 7x5x2 的长方体。**关键**:确保这个最终长方体的形状、尺寸、颜色(所有小方块最终统一成灰色 #808080)和空间方位与场景二最终生成的长方体完全一致。

*   **相机与动画效果**
    *   初始相机视角可能需要调整(例如 `phi=75*DEGREES, theta=15*DEGREES`),以便清晰展示 5x2 切面的构建。
    *   在沿第三个维度扩展时,相机进行平滑旋转和/或缩放 (`move_camera`),最终定格在与场景二结束时**完全相同**的视角和缩放级别,强调两个过程得到了同一个物体。旋转动画时长与扩展动画同步。
    *   左右内容同样保持同步。

---

### 【场景四:对比与结论】

*   **背景与布局**
    *   背景恢复为简洁的纯色背景,例如浅蓝色 (#E0EFFF),或者使用场景一的渐变背景。
    *   屏幕中心展示最终的 7x5x2 长方体(灰色 #808080)。
    *   长方体左右两侧分别展示两种计算路径的最终公式。
    *   屏幕底部留出空间展示结论性文字。
    *   右上角显示场景编号“04”。

*   **内容呈现**
    *   **中心图形**:显示在场景二和场景三中最终形成的、完全相同的 7x5x2 长方体。可以使用 `FadeIn` 效果出现,并可以添加缓慢旋转效果(`Rotating`),让观众从不同角度确认其形态。时长 2 秒。
    *   **左侧公式**:展示
        \[ (7 \times 5) \times 2 = 35 \times 2 = 70 \]
        使用 `Write` 动画效果,从左至右依次出现,时长 1.5 秒。颜色与之前场景保持一致。
    *   **右侧公式**:展示
        \[ 7 \times (5 \times 2) = 7 \times 10 = 70 \]
        使用 `Write` 动画效果,从左至右依次出现,时长 1.5 秒。颜色与之前场景保持一致。
    *   **连接线**:从左侧公式指向中心长方体,再从右侧公式指向中心长方体,画出两条箭头(`Arrow`),使用 `Create` 动画,时长 1 秒。
    *   **核心等式**:在长方体下方或屏幕中央显眼位置,再次展示核心等式:
        \[ (7 \times 5) \times 2 = 7 \times (5 \times 2) \]
        使用 `FadeIn` 效果,并可以对等号 `=` 进行强调(例如放大或闪烁一下)。时长 1 秒。
    *   **结论文字**:在屏幕最下方出现结论性文字:“两种不同的计算顺序,得到了完全相同的几何体和结果 (70)。这直观地证明了乘法结合律。” 文字颜色为黑色 (#000000),使用 `FadeIn` 效果,时长 1.5 秒。

*   **相机与动画效果**
    *   相机聚焦于中心长方体和两侧的公式。如果长方体旋转,相机保持稳定或进行非常缓慢的环绕移动。
    *   整体动画流程:先出现长方体 -> 再出现左右计算公式 -> 画箭头连接 -> 出现核心等式 -> 最后出现结论文字。每个步骤之间有 0.3-0.5 秒的停顿。

---

### 【其他整体要求】

*   **统一视觉风格**
    *   所有场景均采用高清渲染。
    *   所有数学公式均使用 LaTeX `Tex` 或 `MathTex` 精确渲染,保证清晰度。
    *   颜色方案:背景色调柔和(渐变灰白或浅蓝),坐标轴为深灰,文字为黑色或深蓝,代表数字 7, 5, 2 的颜色(红、绿、蓝)在场景二、三中保持一致性,最终组合的长方体使用中性色(如灰色)。重点内容(如高亮公式部分、等号、结论文字)可适当使用醒目颜色或效果。场景编号统一风格。
*   **动画与转场**
    *   场景之间的过渡使用简洁的 `FadeOut` 旧场景元素、`FadeIn` 新场景元素,或者直接替换,保持流畅。过渡时间约 0.5 - 1 秒。
    *   相机运动(旋转、平移、缩放)平滑自然,避免突兀跳动,运动速度适中,确保观众能跟上图形和逻辑的变化。
    *   所有动画(`Create`, `Write`, `FadeIn`, `ReplacementTransform`, `TransformFromCopy`, `Rotating`)的时长和延迟精心设计,与解说节奏匹配(如果后续配音),保证信息传递的有效性。

---

代码

# -*- 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 for AudioFileClip
import manimpango # For font checking

# --- Custom Colors ---
MY_LIGHT_GRAY = "#DDDDDD"
MY_WHITE = "#FFFFFF"
MY_DARK_GRAY = "#555555"
MY_BLACK = "#000000"
MY_DARK_BLUE = "#00008B"
MY_RED_STR = "FF0000" # Hex code without # for LaTeX color
MY_GREEN_STR = "008000"
MY_BLUE_STR = "0000FF"
MY_RED = "#" + MY_RED_STR
MY_GREEN = "#" + MY_GREEN_STR
MY_BLUE = "#" + MY_BLUE_STR
MY_YELLOW_HIGHLIGHT = "#FFFFE0" # For formula highlight background
MY_ORANGE = "#FFA500" # For (7x5) cubes
MY_CYAN = "#00FFFF" # For (5x2) cubes
MY_GRAY_CUBE = "#808080" # Final cube color
MY_LIGHT_BLUE_BG = "#E0EFFF" # Scene 4 background

# --- Font Check ---
DEFAULT_FONT = "Noto Sans CJK SC" # Or another preferred CJK font
final_font = None
available_fonts = manimpango.list_fonts()
if DEFAULT_FONT in available_fonts:
    print(f"字体 '{DEFAULT_FONT}' 已找到。")
    final_font = DEFAULT_FONT
else:
    print(f"警告: 字体 '{DEFAULT_FONT}' 未找到。正在尝试备用字体...")
    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"已切换到备用字体: '{font}'")
            final_font = font
            found_fallback = True
            break
    if not found_fallback:
        print(f"警告: 未找到指定的 '{DEFAULT_FONT}' 或任何备用中文字体。将使用 Manim 默认字体,中文可能无法正确显示。")

# --- TTS Caching Setup ---
CACHE_DIR = "tts_cache"
os.makedirs(CACHE_DIR, exist_ok=True)

class CustomVoiceoverTracker:
    """Tracks audio path and duration for TTS."""
    def __init__(self, audio_path, duration):
        self.audio_path = audio_path
        self.duration = duration

def get_cache_filename(text):
    """Generates a unique filename based on the text hash."""
    text_hash = hashlib.md5(text.encode('utf-8')).hexdigest()
    return os.path.join(CACHE_DIR, f"{text_hash}.mp3")

@contextmanager
def custom_voiceover_tts(text, token="123456", base_url="https://uni-ai.fly.dev/api/manim/tts"):
    """
    Fetches TTS audio, caches it, and provides path and duration.
    Usage: with custom_voiceover_tts("text") as tracker: ...
    """
    cache_file = get_cache_filename(text)
    audio_file = cache_file  # Initialize audio_file

    if os.path.exists(cache_file):
        print(f"Using cached TTS for: {text[:30]}...")
    else:
        print(f"Requesting TTS for: {text[:30]}...")
        try:
            # URL encode the input text to handle special characters
            input_text_encoded = requests.utils.quote(text)
            url = f"{base_url}?token={token}&input={input_text_encoded}"

            response = requests.get(url, stream=True, timeout=60)  # Added timeout
            response.raise_for_status()  # Raise HTTPError for bad responses (4xx or 5xx)

            with open(cache_file, "wb") as f:
                for chunk in response.iter_content(chunk_size=8192):
                    if chunk:
                        f.write(chunk)
            audio_file = cache_file
            print("TTS downloaded and cached.")

        except requests.exceptions.RequestException as e:
            print(f"TTS API request failed: {e}")
            # Fallback: create a dummy tracker with zero duration
            tracker = CustomVoiceoverTracker(None, 0)
            yield tracker
            return  # Exit context manager

    # Ensure audio file exists before processing with MoviePy
    if audio_file and os.path.exists(audio_file):
        try:
            clip = AudioFileClip(audio_file)
            duration = clip.duration
            clip.close()
            print(f"Audio duration: {duration:.2f}s")
            tracker = CustomVoiceoverTracker(audio_file, duration)
        except Exception as e:
            print(f"Error processing audio file {audio_file}: {e}")
            # Fallback if audio file is corrupted or invalid
            tracker = CustomVoiceoverTracker(None, 0)
    else:
        # Fallback if audio file was not created or found
        print(f"TTS audio file not found or not created: {audio_file}")
        tracker = CustomVoiceoverTracker(None, 0)

    try:
        yield tracker
    finally:
        # No cleanup needed here as we are caching
        pass

# --- Custom TeX Template for colorbox/color ---
# Needed for \colorbox and \color[HTML]
# DO NOT include \documentclass here!
color_support_template = TexTemplate(
    preamble=r"""
\usepackage{amsmath}
\usepackage{amssymb}
\usepackage[HTML]{xcolor} % Enable HTML colors and \color / \colorbox
\usepackage{graphicx}
% Add other necessary packages here if needed
"""
)

# --- Combined Scene ---
class CombinedScene(ThreeDScene): # Inherit from ThreeDScene for 3D capabilities

    # Store final objects to carry over if needed (e.g., for comparison)
    final_cubes_s2 = None
    final_axes_s2 = None
    final_labels_s2 = None
    final_dim_labels_s2 = None
    final_cubes_s3 = None
    final_axes_s3 = None
    final_labels_s3 = None
    final_dim_labels_s3 = None

    def setup(self):
        # Set default font if found
        if final_font:
            Text.set_default(font=final_font)
        # Set default TexTemplate for the scene if needed globally
        # Tex.set_default(tex_template=color_support_template)
        # MathTex.set_default(tex_template=color_support_template)
        # Or pass it individually where needed

        # Initial camera setup for 3D scene (can be overridden in scenes)
        self.set_camera_orientation(phi=0 * DEGREES, theta=-90 * DEGREES) # Start looking straight down

    def construct(self):
        self.play_scene_01()
        self.clear_and_reset() # Custom clear for 3D

        self.play_scene_02()
        self.clear_and_reset()

        self.play_scene_03()
        self.clear_and_reset()

        self.play_scene_04()
        self.clear_and_reset()

        # Final message
        self.set_camera_orientation(phi=0, theta=-PI/2) # Reset to 2D-like view
        final_message = Text("动画结束,感谢观看! 😄", font_size=48, color=MY_BLACK)
        # Use a 2D background for the final message
        bg_final = Rectangle(width=config.frame_width, height=config.frame_height, fill_color=MY_LIGHT_BLUE_BG, fill_opacity=1, stroke_width=0).set_z_index(-10)
        # Add fixed in frame mobjects for 2D overlays in 3D scene
        self.add_fixed_in_frame_mobjects(bg_final)
        self.add_fixed_in_frame_mobjects(final_message) # Make message fixed

        self.play(FadeIn(final_message))
        self.wait(2)

    def get_scene_number(self, number_str):
        """Creates and positions the scene number, fixed to the frame."""
        scene_num = Text(number_str, font_size=24, color=MY_DARK_GRAY)
        scene_num.to_corner(UR, buff=0.3)
        scene_num.set_z_index(20) # Ensure above other elements
        # For 3D scenes, make it fixed in frame
        self.add_fixed_in_frame_mobjects(scene_num)
        return scene_num

    def create_gradient_background(self, color1, color2):
        """Creates a gradient background fixed to the frame."""
        # Create a 2D rectangle and add it as a fixed-in-frame mobject
        bg = Rectangle(
            width=config.frame_width * 2, # Make larger to avoid edge issues during rotation
            height=config.frame_height * 2,
            stroke_width=0,
            fill_opacity=1
        )
        # Apply gradient fill using list of colors (default is vertical DOWN)
        # Use set_fill which accepts a list for gradient
        bg.set_fill(color=[color1, color2], opacity=1)
        # Manually set the gradient direction if needed (e.g., vertical)
        # bg.set_sheen_direction(DOWN) # Default is DOWN
        bg.set_z_index(-10)
        # Add fixed in frame so it doesn't move with 3D camera
        self.add_fixed_in_frame_mobjects(bg)
        return bg

    def clear_and_reset(self):
        """Clears all mobjects and resets the camera for a new scene."""
        # *** FIX: Access fixed_in_frame_mobjects via self.camera ***
        all_mobs_to_clear = list(self.mobjects) + list(self.camera.fixed_in_frame_mobjects)

        # Clear updaters from all mobjects
        for mob in all_mobs_to_clear:
            # Check if the mobject exists and has updaters before clearing
            if mob is not None and hasattr(mob, 'get_updaters') and mob.get_updaters():
                mob.clear_updaters()

        # Fade out all mobjects (including fixed ones temporarily)
        # Use Group for potentially mixed object types
        valid_mobs = [m for m in all_mobs_to_clear if m is not None]
        if valid_mobs:
            self.play(FadeOut(Group(*valid_mobs)), run_time=0.5)

        # Clear the lists
        self.mobjects.clear()
        # *** FIX: Clear fixed_in_frame_mobjects via self.camera ***
        self.camera.fixed_in_frame_mobjects.clear()

        # Reset camera for 3D scene to a default state
        self.set_camera_orientation(phi=0 * DEGREES, theta=-90 * DEGREES) # Reset to top-down view
        # Reset zoom/distance and center
        self.move_camera(frame_center=ORIGIN, zoom=1.0, added_anims=[]) # Use move_camera to reset zoom

        self.wait(0.1)

    # --- Scene Implementations ---
    def play_scene_01(self):
        """Scene 1: Title and Problem Introduction"""
        scene_num = self.get_scene_number("01")
        bg = self.create_gradient_background(MY_LIGHT_GRAY, MY_WHITE)

        title = Text("图形化证明:乘法结合律", font_size=48, color=MY_BLACK)
        title.move_to(UP * 2)

        # Formula with colored numbers - requires custom TeX template
        # Use the STR versions of colors (without #) for LaTeX
        formula_str = r"{\color[HTML]{%s} 7} \times {\color[HTML]{%s} 5} \times {\color[HTML]{%s} 2} = {\color[HTML]{%s} 7} \times ({\color[HTML]{%s} 5} \times {\color[HTML]{%s} 2})" % (
            MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR, MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR
        )
        # Pass the custom template here
        formula = MathTex(formula_str, font_size=42, color=MY_DARK_BLUE, tex_template=color_support_template)
        formula.next_to(title, DOWN, buff=0.8)

        # Add elements as fixed in frame for this 2D-like scene
        self.add_fixed_in_frame_mobjects(title)
        self.add_fixed_in_frame_mobjects(formula)

        voice_text_01 = "大家好!本期视频,我们将用图形化的方式来证明乘法的结合律。具体来说,我们要证明 7 乘以 5 再乘以 2,等于 7 乘以 5 乘以 2 的积。"
        with custom_voiceover_tts(voice_text_01) as tracker:
            audio_duration = tracker.duration if tracker.audio_path else 0
            if tracker.audio_path and audio_duration > 0:
                self.add_sound(tracker.audio_path, time_offset=0)
            else:
                print("Warning: Scene 1 TTS failed or has zero duration.")

            subtitle_voice = Text(voice_text_01, font_size=32, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=0.5)
            self.add_fixed_in_frame_mobjects(subtitle_voice) # Fixed subtitle

            anim_title_duration = 1.5
            anim_formula_delay = 0.5
            anim_formula_duration = 1.0
            fade_out_duration = 1.0
            # Calculate total animation time BEFORE fade out
            total_anim_duration_planned = anim_title_duration + anim_formula_delay + anim_formula_duration

            # Use FadeIn for Text as Write might cause issues
            self.play(
                AnimationGroup(
                    FadeIn(subtitle_voice, run_time=0.5), # Subtitle appears first
                    FadeIn(title, shift=UP*0.2), # Use FadeIn for title
                    lag_ratio=0.0
                ),
                run_time=anim_title_duration
            )
            self.wait(anim_formula_delay)
            # Use Write for MathTex as it's a VMobject
            self.play(Write(formula), run_time=anim_formula_duration)

            # Calculate wait time based on audio duration vs animation time
            if audio_duration > 0:
                elapsed_time = total_anim_duration_planned
                time_for_fadeout = fade_out_duration
                remaining_time = audio_duration - elapsed_time - time_for_fadeout
                if remaining_time > 0:
                    self.wait(remaining_time)
            else:
                # If no audio, just wait a bit after animations
                self.wait(2.0)

            # Fade out the voiceover subtitle
            self.play(FadeOut(subtitle_voice), run_time=fade_out_duration)

        self.wait(1)


    def play_scene_02(self):
        """Scene 2: (7x5) x 2 Visualization"""
        scene_num = self.get_scene_number("02")
        bg = self.create_gradient_background(MY_LIGHT_GRAY, MY_WHITE)

        initial_phi = 75 * DEGREES
        initial_theta = -45 * DEGREES
        initial_distance = 15
        self.set_camera_orientation(phi=initial_phi, theta=initial_theta, distance=initial_distance)

        left_zone_x = -config.frame_width / 4
        right_zone_x = config.frame_width / 4

        # --- Left Text & Formulas (Fixed in Frame) ---
        text_title = Text("计算方式一:先算 7 × 5", font_size=30, color=MY_BLACK)
        text_title.move_to([left_zone_x, 3, 0]).align_to([-config.frame_width/2 + 1, 0, 0], LEFT)
        formula_s2_1_str = r"({\color[HTML]{%s} 7} \times {\color[HTML]{%s} 5}) \times {\color[HTML]{%s} 2}" % (MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR)
        formula_s2_1 = MathTex(formula_s2_1_str, font_size=36, color=MY_DARK_BLUE, tex_template=color_support_template)
        formula_s2_1.next_to(text_title, DOWN, buff=0.5, aligned_edge=LEFT)
        formula_s2_2_str = r"{\color[HTML]{%s} 7} \times {\color[HTML]{%s} 5} = 35" % (MY_RED_STR, MY_GREEN_STR)
        formula_s2_2 = MathTex(formula_s2_2_str, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_s2_2.next_to(formula_s2_1, DOWN, buff=0.5, aligned_edge=LEFT)
        formula_s2_3_str = r"= 35 \times {\color[HTML]{%s} 2} = 70" % MY_BLUE_STR
        formula_s2_3 = MathTex(formula_s2_3_str, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_s2_3.move_to(formula_s2_1).align_to(formula_s2_1, LEFT)
        left_group = VGroup(text_title, formula_s2_1, formula_s2_2)
        self.add_fixed_in_frame_mobjects(left_group)
        formula_s2_2.set_opacity(0)
        # highlight_rect created later

        # --- Right 3D Visualization ---
        axes = ThreeDAxes(
            x_range=[0, 8, 2], y_range=[0, 6, 2], z_range=[0, 3, 1],
            x_length=7, y_length=5, z_length=3,
            axis_config={"color": MY_DARK_GRAY, "include_tip": False, "stroke_width": 2, "include_numbers": True, "decimal_number_config": {"num_decimal_places": 0}},
            x_axis_config={"color": MY_RED}, y_axis_config={"color": MY_GREEN}, z_axis_config={"color": MY_BLUE},
        )
        axes.move_to([right_zone_x, 0, 0])
        labels = axes.get_axis_labels(
            x_label=Tex("7", color=MY_RED), y_label=Tex("5", color=MY_GREEN), z_label=Tex("2", color=MY_BLUE)
        )

        # *** FIX: Add axes and labels to the scene explicitly first ***
        self.add(axes, labels)
        # Make them initially invisible if FadeIn animation is desired
        axes.set_opacity(0)
        labels.set_opacity(0)

        # Cube properties
        cube_size = 0.5
        gap = 0.0
        base_layer = VGroup()
        x_offset = - (7 - 1) * (cube_size + gap) / 2
        y_offset = - (5 - 1) * (cube_size + gap) / 2
        z_offset = cube_size / 2
        for i in range(7):
            for j in range(5):
                cube = Cube(side_length=cube_size, fill_opacity=0.8, stroke_width=0.5, stroke_color=MY_DARK_GRAY)
                x_pos = i * (cube_size + gap) + x_offset
                y_pos = j * (cube_size + gap) + y_offset
                z_pos = z_offset
                cube.move_to(axes.get_origin() + RIGHT * x_pos + UP * y_pos + OUT * (z_pos - cube_size/2))
                cube.set_fill(MY_ORANGE)
                base_layer.add(cube)
        top_layer = base_layer.copy()
        top_layer.shift(OUT * (cube_size + gap))
        top_layer.set_fill(MY_BLUE)
        # cubes_group = VGroup(base_layer, top_layer) # Group for potential later use

        # --- Animations ---
        voice_text_02 = "现在来看第一种计算方式,我们先计算 7 乘以 5。在右边的三维空间里,我们构建一个 7 行 5 列的底层,由 35 个小方块组成,代表 7 乘以 5 等于 35。接着,我们将这个底层向上堆叠一层,代表乘以 2。这样,我们就得到了一个 7 乘 5 乘 2 的长方体,总共有 35 乘以 2,等于 70 个小方块。"
        with custom_voiceover_tts(voice_text_02) as tracker:
            audio_duration = tracker.duration if tracker.audio_path else 0
            if tracker.audio_path and audio_duration > 0:
                self.add_sound(tracker.audio_path, time_offset=0)
            else: print("Warning: Scene 2 TTS failed or has zero duration.")

            subtitle_voice = Text(voice_text_02, font_size=32, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=0.5)
            self.add_fixed_in_frame_mobjects(subtitle_voice)

            anim_intro_duration = 1.5
            anim_base_duration = 2.0
            anim_stack_duration = 1.5
            fade_out_duration = 1.0

            target_highlight = VGroup(formula_s2_1.get_part_by_tex("7"), formula_s2_1.get_part_by_tex("5"), formula_s2_1.get_part_by_tex(r"\times")[0])
            highlight_rect = SurroundingRectangle(target_highlight, buff=0.05, color=MY_YELLOW_HIGHLIGHT, fill_color=MY_YELLOW_HIGHLIGHT, fill_opacity=0.3, stroke_width=0)
            self.add_fixed_in_frame_mobjects(highlight_rect)
            highlight_rect.set_opacity(0)

            # *** FIX: Animate FadeIn for already added axes/labels ***
            self.play(
                AnimationGroup(
                    FadeIn(subtitle_voice, run_time=0.5),
                    FadeIn(axes), # FadeIn the axes
                    FadeIn(labels), # FadeIn the labels
                    FadeIn(text_title),
                    Write(formula_s2_1),
                    FadeIn(highlight_rect),
                    lag_ratio=0.0
                ),
                run_time=anim_intro_duration
            )

            self.play(
                AnimationGroup(
                    Create(base_layer, lag_ratio=0.01),
                    FadeIn(formula_s2_2, shift=DOWN*0.2),
                    lag_ratio=0.1
                ),
                run_time=anim_base_duration
            )

            self.move_camera(phi=65 * DEGREES, theta=-55 * DEGREES, distance=initial_distance * 1.1, run_time=anim_stack_duration)
            self.play(
                AnimationGroup(
                    TransformFromCopy(base_layer, top_layer),
                    ReplacementTransform(VGroup(formula_s2_1, highlight_rect), formula_s2_3),
                    FadeOut(formula_s2_2),
                    lag_ratio=0.1
                ),
                run_time=anim_stack_duration
            )

            total_anim_time = anim_intro_duration + anim_base_duration + anim_stack_duration
            if audio_duration > 0:
                remaining_time = audio_duration - total_anim_time - fade_out_duration
                if remaining_time > 0: self.wait(remaining_time)
            else: self.wait(1.5)

            self.play(FadeOut(subtitle_voice), run_time=fade_out_duration)

        self.wait(1)
        CombinedScene.final_cubes_s2 = VGroup(base_layer, top_layer)
        CombinedScene.final_axes_s2 = axes
        CombinedScene.final_labels_s2 = labels

    def play_scene_03(self):
        """Scene 3: 7 x (5x2) Visualization"""
        scene_num = self.get_scene_number("03")
        bg = self.create_gradient_background(MY_LIGHT_GRAY, MY_WHITE)

        initial_phi = 75 * DEGREES
        initial_theta = 15 * DEGREES
        initial_distance = 15
        self.set_camera_orientation(phi=initial_phi, theta=initial_theta, distance=initial_distance)

        left_zone_x = -config.frame_width / 4
        right_zone_x = config.frame_width / 4

        # --- Left Text & Formulas (Fixed in Frame) ---
        text_title_s3 = Text("计算方式二:先算 5 × 2", font_size=30, color=MY_BLACK)
        text_title_s3.move_to([left_zone_x, 3, 0]).align_to([-config.frame_width/2 + 1, 0, 0], LEFT)
        formula_s3_1_str = r"{\color[HTML]{%s} 7} \times ({\color[HTML]{%s} 5} \times {\color[HTML]{%s} 2})" % (MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR)
        formula_s3_1 = MathTex(formula_s3_1_str, font_size=36, color=MY_DARK_BLUE, tex_template=color_support_template)
        formula_s3_1.next_to(text_title_s3, DOWN, buff=0.5, aligned_edge=LEFT)
        formula_s3_2_str = r"{\color[HTML]{%s} 5} \times {\color[HTML]{%s} 2} = 10" % (MY_GREEN_STR, MY_BLUE_STR)
        formula_s3_2 = MathTex(formula_s3_2_str, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_s3_2.next_to(formula_s3_1, DOWN, buff=0.5, aligned_edge=LEFT)
        formula_s3_3_str = r"= {\color[HTML]{%s} 7} \times 10 = 70" % MY_RED_STR
        formula_s3_3 = MathTex(formula_s3_3_str, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_s3_3.move_to(formula_s3_1).align_to(formula_s3_1, LEFT)
        left_group_s3 = VGroup(text_title_s3, formula_s3_1, formula_s3_2)
        self.add_fixed_in_frame_mobjects(left_group_s3)
        formula_s3_2.set_opacity(0)
        # highlight_rect_s3 created later

        # --- Right 3D Visualization ---
        axes_s3 = ThreeDAxes(
            x_range=[0, 8, 2], y_range=[0, 6, 2], z_range=[0, 3, 1],
            x_length=7, y_length=5, z_length=3,
            axis_config={"color": MY_DARK_GRAY, "include_tip": False, "stroke_width": 2, "include_numbers": True, "decimal_number_config": {"num_decimal_places": 0}},
            x_axis_config={"color": MY_RED}, y_axis_config={"color": MY_GREEN}, z_axis_config={"color": MY_BLUE},
        )
        axes_s3.move_to([right_zone_x, 0, 0])
        labels_s3 = axes_s3.get_axis_labels(
            x_label=Tex("7", color=MY_RED), y_label=Tex("5", color=MY_GREEN), z_label=Tex("2", color=MY_BLUE)
        )

        # *** FIX: Add axes and labels to the scene explicitly first ***
        self.add(axes_s3, labels_s3)
        axes_s3.set_opacity(0)
        labels_s3.set_opacity(0)


        cube_size = 0.5
        gap = 0.0
        slice_layer = VGroup()
        x_offset_s3 = - (7 - 1) * (cube_size + gap) / 2
        y_offset_s3 = - (5 - 1) * (cube_size + gap) / 2
        z_offset_s3 = - (2 - 1) * (cube_size + gap) / 2
        first_slice_x = x_offset_s3 + cube_size / 2
        for j in range(5):
            for k in range(2):
                cube = Cube(side_length=cube_size, fill_opacity=0.8, stroke_width=0.5, stroke_color=MY_DARK_GRAY)
                x_pos = first_slice_x
                y_pos = j * (cube_size + gap) + y_offset_s3 + cube_size / 2
                z_pos = k * (cube_size + gap) + z_offset_s3 + cube_size / 2
                cube.move_to(axes_s3.get_origin() + RIGHT * x_pos + UP * y_pos + OUT * z_pos)
                cube.set_fill(MY_CYAN)
                slice_layer.add(cube)

        full_block = VGroup()
        all_slices = []
        for i in range(7):
            new_slice = slice_layer.copy()
            x_shift = i * (cube_size + gap)
            actual_shift = RIGHT * x_shift
            new_slice.shift(actual_shift)
            new_slice.set_fill(MY_GRAY_CUBE)
            full_block.add(new_slice)
            all_slices.append(new_slice)

        # --- Animations ---
        voice_text_03 = "接下来,我们换一种方式,先计算括号里的 5 乘以 2。我们在 YZ 平面上构建一个 5 行 2 列的切片,由 10 个小方块组成,代表 5 乘以 2 等于 10。然后,我们将这个切片沿着 X 轴方向扩展 7 次,代表乘以 7。最终,我们同样得到了一个 7 乘 5 乘 2 的长方体,总共有 7 乘以 10,等于 70 个小方块。请注意,这个长方体和上一种方法得到的是完全一样的!"
        with custom_voiceover_tts(voice_text_03) as tracker:
            audio_duration = tracker.duration if tracker.audio_path else 0
            if tracker.audio_path and audio_duration > 0:
                self.add_sound(tracker.audio_path, time_offset=0)
            else: print("Warning: Scene 3 TTS failed or has zero duration.")

            subtitle_voice = Text(voice_text_03, font_size=32, color=MY_BLACK, width=config.frame_width - 2, should_center=True).to_edge(DOWN, buff=0.5)
            self.add_fixed_in_frame_mobjects(subtitle_voice)

            anim_intro_duration = 1.5
            anim_slice_duration = 1.5
            anim_extend_duration = 2.0
            fade_out_duration = 1.0

            target_highlight_s3 = VGroup(formula_s3_1.get_part_by_tex("5"), formula_s3_1.get_part_by_tex("2"), formula_s3_1.get_part_by_tex(r"\times")[1])
            highlight_rect_s3 = SurroundingRectangle(target_highlight_s3, buff=0.05, color=MY_YELLOW_HIGHLIGHT, fill_color=MY_YELLOW_HIGHLIGHT, fill_opacity=0.3, stroke_width=0)
            self.add_fixed_in_frame_mobjects(highlight_rect_s3)
            highlight_rect_s3.set_opacity(0)

            # *** FIX: Animate FadeIn for already added axes/labels ***
            self.play(
                AnimationGroup(
                    FadeIn(subtitle_voice, run_time=0.5),
                    FadeIn(axes_s3), # FadeIn the axes
                    FadeIn(labels_s3), # FadeIn the labels
                    FadeIn(text_title_s3),
                    Write(formula_s3_1),
                    FadeIn(highlight_rect_s3),
                    lag_ratio=0.0
                ),
                run_time=anim_intro_duration
            )

            self.play(
                AnimationGroup(
                    Create(slice_layer, lag_ratio=0.05),
                    FadeIn(formula_s3_2, shift=DOWN*0.2),
                    lag_ratio=0.1
                ),
                run_time=anim_slice_duration
            )

            final_phi = 65 * DEGREES
            final_theta = -55 * DEGREES
            final_distance = initial_distance * 1.1
            self.move_camera(phi=final_phi, theta=final_theta, distance=final_distance, run_time=anim_extend_duration)
            self.play(
                AnimationGroup(
                    LaggedStart(*[TransformFromCopy(slice_layer if i==0 else all_slices[i-1], s) for i, s in enumerate(all_slices)], lag_ratio=0.1),
                    ReplacementTransform(VGroup(formula_s3_1, highlight_rect_s3), formula_s3_3),
                    FadeOut(formula_s3_2),
                    FadeOut(slice_layer, run_time=0.1),
                    lag_ratio=0.1
                ),
                run_time=anim_extend_duration
            )

            total_anim_time = anim_intro_duration + anim_slice_duration + anim_extend_duration
            if audio_duration > 0:
                remaining_time = audio_duration - total_anim_time - fade_out_duration
                if remaining_time > 0: self.wait(remaining_time)
            else: self.wait(1.5)

            self.play(FadeOut(subtitle_voice), run_time=fade_out_duration)

        self.wait(1)
        CombinedScene.final_cubes_s3 = full_block
        CombinedScene.final_axes_s3 = axes_s3
        CombinedScene.final_labels_s3 = labels_s3

    def play_scene_04(self):
        """Scene 4: Comparison and Conclusion"""
        scene_num = self.get_scene_number("04")
        # Use a plain background fixed in frame
        bg = Rectangle(width=config.frame_width*2, height=config.frame_height*2, fill_color=MY_LIGHT_BLUE_BG, fill_opacity=1, stroke_width=0).set_z_index(-10)
        self.add_fixed_in_frame_mobjects(bg)

        # Camera: Stable view, matching the end view of Scene 2 & 3
        final_phi = 65 * DEGREES
        final_theta = -55 * DEGREES
        final_distance = 15 * 1.1 # Match end distance
        self.set_camera_orientation(phi=final_phi, theta=final_theta, distance=final_distance)

        # --- Center: Final Cube ---
        # Use the cube from scene 3 (or scene 2, they should be identical)
        # Ensure it's colored gray
        if CombinedScene.final_cubes_s3:
             # Need to ensure the object exists before copying
            final_cube_display = CombinedScene.final_cubes_s3.copy()
        elif CombinedScene.final_cubes_s2:
            final_cube_display = CombinedScene.final_cubes_s2.copy()
        else:
            # Fallback: create a dummy cube if previous scenes failed
            print("Warning: Could not retrieve final cubes from previous scenes.")
            final_cube_display = Cube() # Placeholder

        final_cube_display.set_fill(MY_GRAY_CUBE, opacity=0.8)
        final_cube_display.move_to(ORIGIN) # Center it in 3D space

        # --- Left Formula (Fixed) ---
        formula_left_str1 = r"({\color[HTML]{%s} 7} \times {\color[HTML]{%s} 5}) \times {\color[HTML]{%s} 2}" % (MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR)
        formula_left_str2 = r"= 35 \times {\color[HTML]{%s} 2} = 70" % MY_BLUE_STR
        formula_left1 = MathTex(formula_left_str1, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_left2 = MathTex(formula_left_str2, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_left_group = VGroup(formula_left1, formula_left2).arrange(DOWN, aligned_edge=LEFT)
        # Position fixed relative to the frame
        formula_left_group.to_corner(LEFT + UP, buff=1.5)

        # --- Right Formula (Fixed) ---
        formula_right_str1 = r"{\color[HTML]{%s} 7} \times ({\color[HTML]{%s} 5} \times {\color[HTML]{%s} 2})" % (MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR)
        formula_right_str2 = r"= {\color[HTML]{%s} 7} \times 10 = 70" % MY_RED_STR
        formula_right1 = MathTex(formula_right_str1, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_right2 = MathTex(formula_right_str2, font_size=36, color=MY_BLACK, tex_template=color_support_template)
        formula_right_group = VGroup(formula_right1, formula_right2).arrange(DOWN, aligned_edge=LEFT)
        # Position fixed relative to the frame
        formula_right_group.to_corner(RIGHT + UP, buff=1.5)

        # --- Arrows (Fixed) ---
        # Arrows need to connect fixed formulas to the 3D cube's projected position
        # Draw arrows from formula groups towards the center (ORIGIN in fixed frame)
        arrow_left = Arrow(formula_left_group.get_right(), ORIGIN + LEFT*2, buff=0.2, color=MY_DARK_GRAY)
        arrow_right = Arrow(formula_right_group.get_left(), ORIGIN + RIGHT*2, buff=0.2, color=MY_DARK_GRAY)

        # --- Core Equality (Fixed) ---
        core_eq_str = r"({\color[HTML]{%s} 7} \times {\color[HTML]{%s} 5}) \times {\color[HTML]{%s} 2} = {\color[HTML]{%s} 7} \times ({\color[HTML]{%s} 5} \times {\color[HTML]{%s} 2})" % (
            MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR, MY_RED_STR, MY_GREEN_STR, MY_BLUE_STR
        )
        core_equality = MathTex(core_eq_str, font_size=42, color=MY_DARK_BLUE, tex_template=color_support_template)
        # Position below the cube's projected area
        core_equality.move_to(DOWN * 1.5)

        # --- Conclusion Text (Fixed) ---
        conclusion_text = Text(
            "两种不同的计算顺序,得到了完全相同的几何体和结果 (70)。\n这直观地证明了乘法结合律。",
            font_size=32, color=MY_BLACK, line_spacing=0.8, should_center=True
        )
        conclusion_text.to_edge(DOWN, buff=0.8)

        # Add elements
        self.add(final_cube_display) # The 3D cube itself is not fixed
        fixed_elements = VGroup(formula_left_group, formula_right_group, arrow_left, arrow_right, core_equality, conclusion_text)
        self.add_fixed_in_frame_mobjects(fixed_elements)
        # Hide elements initially
        fixed_elements.set_opacity(0)


        # --- Animations ---
        voice_text_04 = "最后我们来对比一下。左边是先算 7 乘 5,右边是先算 5 乘 2。虽然计算过程不同,但我们最终都得到了中间这个完全相同的、由 70 个小方块组成的灰色长方体。这表明,(7 乘以 5) 再乘以 2,等于 7 乘以 (5 乘以 2)。两种不同的计算顺序,得到了完全相同的几何体和结果 70。这直观地证明了乘法结合律。"
        with custom_voiceover_tts(voice_text_04) as tracker:
            audio_duration = tracker.duration if tracker.audio_path else 0
            if tracker.audio_path and audio_duration > 0:
                self.add_sound(tracker.audio_path, time_offset=0)
            else: print("Warning: Scene 4 TTS failed or has zero duration.")

            subtitle_voice = Text(voice_text_04, font_size=32, color=MY_BLACK, width=config.frame_width - 2, should_center=True)
            # Position subtitle above conclusion text
            subtitle_voice.next_to(conclusion_text, UP, buff=0.3)
            self.add_fixed_in_frame_mobjects(subtitle_voice)
            subtitle_voice.set_opacity(0) # Hide initially

            # Timings
            anim_cube_fadein = 1.5
            anim_rotate_start_delay = 0.5
            anim_rotate_duration = 8.0 # Slow rotation duration (used for wait calculation)
            anim_formulas_duration = 1.5
            anim_arrows_duration = 1.0
            anim_core_eq_duration = 1.0
            anim_conclusion_duration = 1.5
            fade_out_duration = 1.0

            # Fade in cube and subtitle
            self.play(
                FadeIn(final_cube_display),
                FadeIn(subtitle_voice, run_time=0.5), # Show subtitle early
                run_time=anim_cube_fadein
            )

            # Add rotation updater after a delay
            self.wait(anim_rotate_start_delay)
            # Define rotation speed (radians per second)
            rotation_speed = 0.2 # Radians per second
            final_cube_display.add_updater(lambda m, dt: m.rotate(rotation_speed * dt, axis=UP))

            # Show formulas, arrows, core equality, conclusion
            self.play(FadeIn(formula_left_group, shift=RIGHT*0.5), FadeIn(formula_right_group, shift=LEFT*0.5), run_time=anim_formulas_duration)
            self.play(Create(arrow_left), Create(arrow_right), run_time=anim_arrows_duration)
            self.play(Write(core_equality), run_time=anim_core_eq_duration) # Use Write for MathTex
            # Highlight equals sign
            self.play(Indicate(core_equality.get_part_by_tex("="), scale_factor=1.5, color=MY_RED))
            self.play(FadeIn(conclusion_text, shift=UP*0.3), run_time=anim_conclusion_duration)

            # Calculate wait time based on audio, considering rotation is ongoing
            # Time spent on main animations (excluding rotation start delay)
            main_anim_time = anim_cube_fadein + anim_formulas_duration + anim_arrows_duration + anim_core_eq_duration + 0.5 + anim_conclusion_duration # Added 0.5 for Indicate
            if audio_duration > 0:
                remaining_time = audio_duration - main_anim_time - fade_out_duration
                if remaining_time > 0:
                    self.wait(remaining_time)
            else:
                # Wait for a reasonable time if no audio
                self.wait(max(0, anim_rotate_duration - main_anim_time)) # Wait roughly for rotation duration

            # Stop rotation before fading out
            # Check if updater exists before clearing
            if final_cube_display.get_updaters():
                 final_cube_display.clear_updaters()

            self.play(FadeOut(subtitle_voice), run_time=fade_out_duration)

        self.wait(2) # Hold final screen


# --- Main execution block ---
if __name__ == "__main__":
    config.pixel_height = 1080
    config.pixel_width = 1920
    config.frame_rate = 30
    config.output_file = "CombinedScene"
    config.disable_caching = True
    # Use placeholder for output path - IMPORTANT: Use raw string or double backslashes if needed on Windows
    config.media_dir = r"12" # Java will replace this placeholder

    scene = CombinedScene()
    scene.render()
    print(f"Scene rendering finished. Output in: {config.media_dir}")
Edit this page
Last Updated:
Contributors: Tong Li