From 32c63bf407b76bd59b1a08df0467c25654af18c2 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:00:45 +0800 Subject: [PATCH 01/26] Delete manimlib/scene/__init__.py --- manimlib/scene/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 manimlib/scene/__init__.py diff --git a/manimlib/scene/__init__.py b/manimlib/scene/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 From 2048adf35647a8fb58130a55fa6c73733440703b Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:10:42 +0800 Subject: [PATCH 02/26] Update scene.py --- manimlib/scene/scene.py | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 79745ca6cc..48f7a2aa8a 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -50,7 +50,11 @@ from PIL.Image import Image from manimlib.animation.animation import Animation - +''' +typing模块中的Callable、Iterable和Vect3类型以及PIL.Image模块中的Image类 +和manimlib.animation.animation模块中的Animation类只在类型检查时才会被导入, +而在实际运行时不会被引入,从而避免了循环导入问题 +''' PAN_3D_KEY = 'd' FRAME_SHIFT_KEY = 'f' @@ -71,6 +75,22 @@ class Scene(object): # Euler angles, in degrees default_frame_orientation = (0, 0) + + '''' + 这个构造函数用于创建一个对象,该对象包含了一系列配置参数,用于控制动画的播放和展示效果。 + camera_config:摄像头配置参数,是一个字典,默认为空字典。 + file_writer_config:文件写入器配置参数,是一个字典,默认为空字典。 + skip_animations:是否跳过动画,默认为False。 + always_update_mobjects:是否始终更新对象,默认为False。 + start_at_animation_number:开始动画的编号,默认为None。 + end_at_animation_number:结束动画的编号,默认为None。 + leave_progress_bars:是否保留进度条,默认为False。 + preview:是否预览,默认为True。 + presenter_mode:演示者模式,默认为False。 + show_animation_progress:是否显示动画进度,默认为False。 + embed_exception_mode:嵌入式异常模式,默认为空字符串。 + embed_error_sound:是否嵌入错误声音,默认为False。 + ''' def __init__( self, window_config: dict = dict(), @@ -87,6 +107,9 @@ def __init__( embed_exception_mode: str = "", embed_error_sound: bool = False, ): + self.camera_config = {**self.default_camera_config, **camera_config} + self.window_config = {**self.default_window_config, **window_config} + self.skip_animations = skip_animations self.always_update_mobjects = always_update_mobjects self.start_at_animation_number = start_at_animation_number @@ -98,8 +121,7 @@ def __init__( self.embed_exception_mode = embed_exception_mode self.embed_error_sound = embed_error_sound - self.camera_config = {**self.default_camera_config, **camera_config} - self.window_config = {**self.default_window_config, **window_config} + for config in self.camera_config, self.window_config: config["samples"] = self.samples self.file_writer_config = {**self.default_file_writer_config, **file_writer_config} From c3b5e46f43cf4873b30a634e8f9c43871d6feb4c Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Thu, 15 Feb 2024 20:19:53 +0800 Subject: [PATCH 03/26] Update config.py --- manimlib/config.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/config.py b/manimlib/config.py index 1de7bbd7cd..d76b24a91d 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -25,13 +25,17 @@ def parse_cli(): try: + # 对象parser,用于解析命令行参数 parser = argparse.ArgumentParser() + # 互斥组module_location,用于处理互斥的参数选项 module_location = parser.add_mutually_exclusive_group() + # 向互斥组中添加了一个参数选项file,指定一个文件路径,用于存放场景的Python代码 module_location.add_argument( "file", nargs="?", help="Path to file holding the python code for the scene", ) + # 同上,指定要查看的场景类的名称 parser.add_argument( "scene_names", nargs="*", From 32a33c6bdb0d5fdb028612a13311fc10644dbcd1 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:13:45 +0800 Subject: [PATCH 04/26] Update scene.py --- manimlib/scene/scene.py | 119 ++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 36 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 48f7a2aa8a..7bd7305fa6 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -55,44 +55,43 @@ 和manimlib.animation.animation模块中的Animation类只在类型检查时才会被导入, 而在实际运行时不会被引入,从而避免了循环导入问题 ''' - +# 3D场景平移操作 PAN_3D_KEY = 'd' +# 切换场景帧 FRAME_SHIFT_KEY = 'f' +# 重置场景帧 RESET_FRAME_KEY = 'r' +# 退出交互模式 QUIT_KEY = 'q' class Scene(object): + # 随机种子,初始为0 random_seed: int = 0 + # 平移灵敏度,初始为0.5 pan_sensitivity: float = 0.5 + # 滚动灵敏度,初始为0.5 scroll_sensitivity: float = 20 + # 拖动平移开关,初始打开 drag_to_pan: bool = True + # 最大保存状态数,初始为50 max_num_saved_states: int = 50 + # 默认摄像头配置,初始为空字典 default_camera_config: dict = dict() + # 默认窗口配置,初始为空字典 default_window_config: dict = dict() + # 默认文件写入器配置,初始为空字典 default_file_writer_config: dict = dict() + # 样本数,初始为0 samples = 0 - # Euler angles, in degrees + # 默认帧方向,欧拉角,以度为单位,初始为(0, 0),即不进行任何旋转 default_frame_orientation = (0, 0) - - '''' - 这个构造函数用于创建一个对象,该对象包含了一系列配置参数,用于控制动画的播放和展示效果。 - camera_config:摄像头配置参数,是一个字典,默认为空字典。 - file_writer_config:文件写入器配置参数,是一个字典,默认为空字典。 - skip_animations:是否跳过动画,默认为False。 - always_update_mobjects:是否始终更新对象,默认为False。 - start_at_animation_number:开始动画的编号,默认为None。 - end_at_animation_number:结束动画的编号,默认为None。 - leave_progress_bars:是否保留进度条,默认为False。 - preview:是否预览,默认为True。 - presenter_mode:演示者模式,默认为False。 - show_animation_progress:是否显示动画进度,默认为False。 - embed_exception_mode:嵌入式异常模式,默认为空字符串。 - embed_error_sound:是否嵌入错误声音,默认为False。 - ''' + # 创建一个Scene场景对象,该对象包含了一系列配置参数,用于控制动画的播放和展示效果。 def __init__( self, + # camera_config:摄像头配置参数,是一个字典,默认为空字典。 + # file_writer_config:文件写入器配置参数,是一个字典,默认为空字典。 window_config: dict = dict(), camera_config: dict = dict(), file_writer_config: dict = dict(), @@ -107,9 +106,20 @@ def __init__( embed_exception_mode: str = "", embed_error_sound: bool = False, ): + # 将默认相机配置和传入的相机配置合并,确保所有参数都被正确设置 self.camera_config = {**self.default_camera_config, **camera_config} + # 将默认窗口配置和传入的窗口配置合并,以便在创建窗口时使用这些配置 self.window_config = {**self.default_window_config, **window_config} - + # skip_animations:是否跳过动画,默认为False。 + # always_update_mobjects:是否始终更新对象,默认为False。 + # start_at_animation_number:开始动画的编号,默认为None。 + # end_at_animation_number:结束动画的编号,默认为None。 + # leave_progress_bars:是否保留进度条,默认为False。 + # preview:是否预览,默认为True。 + # presenter_mode:演示者模式,默认为False。 + # show_animation_progress:是否显示动画进度,默认为False。 + # embed_exception_mode:嵌入式异常模式,默认为空字符串。 + # embed_error_sound:是否嵌入错误声音,默认为False。 self.skip_animations = skip_animations self.always_update_mobjects = always_update_mobjects self.start_at_animation_number = start_at_animation_number @@ -121,26 +131,47 @@ def __init__( self.embed_exception_mode = embed_exception_mode self.embed_error_sound = embed_error_sound - + # 为相机配置、窗口配置和文件写入器配置设置一些默认值 for config in self.camera_config, self.window_config: + # 控制渲染时的采样级别,影响渲染质量 config["samples"] = self.samples + # 将文件写入器配置更新为默认文件写入器配置和传入的文件写入器配置的组合。这里使用了字典解包(**)的方式将两个配置合并 self.file_writer_config = {**self.default_file_writer_config, **file_writer_config} - # Initialize window, if applicable + # 是否需要预览场景 if self.preview: from manimlib.window import Window + # 将当前场景传递给窗口对象,将窗口配置传递给窗口对象的构造函数 self.window = Window(scene=self, **self.window_config) + # 将窗口对象添加到相机配置参数中,以便在渲染时使用 self.camera_config["window"] = self.window - self.camera_config["fps"] = 30 # Where's that 30 from? + # 相机的帧率为30帧每秒 + self.camera_config["fps"] = 30 # 30从哪里来的? else: + # 如果不需要预览场景,则没有窗口对象 self.window = None - # Core state of the scene + # 场景类的核心状态定义,包括摄像头、帧、文件写入器等。 + # 这些状态记录了场景的基本信息和状态,用于控制场景的展示和交互。 + # camera:摄像头对象,用于控制场景的视角和视野。 + # frame:摄像头的帧,描述了场景中物体的位置和视角。 + # 根据字典中的配置信息创建一个相机对象 self.camera: Camera = Camera(**self.camera_config) + # 获取相机的帧对象 self.frame: CameraFrame = self.camera.frame + # 将相机帧的方向设置为默认帧方向 self.frame.reorient(*self.default_frame_orientation) + # 将当前方向设置为默认方向 self.frame.make_orientation_default() - + # file_writer:文件写入器,用于将场景渲染为视频或图片。 + # mobjects:场景中的物体列表,包括摄像头帧和其他添加的物体。 + # render_groups:渲染组,用于控制物体的渲染顺序和分组。 + # id_to_mobject_map:物体ID映射表,记录了物体和其对应的ID的关系。 + # num_plays:场景播放次数计数器。 + # time:场景播放的时间。 + # skip_time:跳过动画的时间。 + # original_skipping_status:初始跳过动画的状态。 + # checkpoint_states:场景状态检查点,记录了场景的历史状态。 self.file_writer = SceneFileWriter(self, **self.file_writer_config) self.mobjects: list[Mobject] = [self.camera.frame] self.render_groups: list[Mobject] = [] @@ -150,25 +181,33 @@ def __init__( self.skip_time: float = 0 self.original_skipping_status: bool = self.skip_animations self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict() + # undo_stack和redo_stack:撤销和重做栈,用于撤销和重做操作。 self.undo_stack = [] self.redo_stack = [] + # 如果在场景配置中指定了开始动画的序号,则在渲染场景时会跳过前面的动画,直接从指定序号开始播放动画 if self.start_at_animation_number is not None: self.skip_animations = True + # 如果场景配置中指定了显示进度条,则不显示动画进度条 if self.file_writer.has_progress_display(): self.show_animation_progress = False - # Items associated with interaction + # 一些交互相关物件 + # 鼠标的当前位置 self.mouse_point = Point() + # 鼠标拖拽的起始位置 self.mouse_drag_point = Point() + # 是否在演示者模式下暂停场景,否,则不暂停 self.hold_on_wait = self.presenter_mode + # 是否退出交互模式,否,则继续交互 self.quit_interaction = False - # Much nicer to work with deterministic scenes + # # 设置随机数生成的种子,以确保在需要确定性行为的场景中能够重现相同的结果 if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) - + + # 返回了场景类的类名 def __str__(self) -> str: return self.__class__.__name__ @@ -208,22 +247,23 @@ def tear_down(self) -> None: if self.window: self.window.destroy() self.window = None - + + # 与场景进行交互 def interact(self) -> None: - """ - If there is a window, enter a loop - which updates the frame while under - the hood calling the pyglet event loop - """ + # 如果没有窗口则直接返回 if self.window is None: return + # 使用按键d、f或z与场景交互,按下command + q或esc退出 log.info( "\nTips: Using the keys `d`, `f`, or `z` " + "you can interact with the scene. " + "Press `command + q` or `esc` to quit" ) + # 不会跳过动画 self.skip_animations = False + # 在窗口没有关闭的情况下不断更新帧 while not self.is_window_closing(): + # 更新帧,参数为每秒帧数的倒数 self.update_frame(1 / self.camera.fps) def embed( @@ -1035,18 +1075,25 @@ def restore_scene(self, scene: Scene): for mob, mob_copy in self.mobjects_to_copies.items() ] - +# 在某些场景结束时捕获该异常并执行一些清理工作或者切换到下一个场景 class EndScene(Exception): pass - +# 创建一个三维场景 class ThreeDScene(Scene): + # 渲染三维对象时用于采样的数量。增加采样数量可以提高图像质量,但也会增加计算量和渲染时间 samples = 4 + # 默认的摄像头视角,是一个二元组,表示摄像头的旋转角度,通常用来调整场景的初始视角 default_frame_orientation = (-30, 70) + # 是否始终启用深度测试 always_depth_test = True - + # 向场景中添加三维对象 def add(self, *mobjects, set_depth_test: bool = True): + # 遍历所有传入的物体 mobjects,检查是否需要对深度进行测试 for mob in mobjects: + # 如果 set_depth_test 为 True,并且物体 mob 不是固定在帧中的,并且 always_depth_test 属性为 True: if set_depth_test and not mob.is_fixed_in_frame() and self.always_depth_test: + # 则调用 mob.apply_depth_test() 方法对物体应用深度测试 mob.apply_depth_test() + # 将所有物体添加到场景中 super().add(*mobjects) From b9775d7b4837b198ea4fe8f4250a29e8ab320336 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:28:36 +0800 Subject: [PATCH 05/26] Update scene_file_writer.py --- manimlib/scene/scene_file_writer.py | 31 +++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index eca49b05ef..1904b7ac8d 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -26,7 +26,7 @@ from manimlib.camera.camera import Camera from manimlib.scene.scene import Scene - +# 用于处理场景文件写入的类,接受多个参数来配置文件的写入行为 class SceneFileWriter(object): def __init__( self, @@ -53,6 +53,26 @@ def __init__( saturation: float = 1.0, gamma: float = 1.0, ): + # scene: 该场景的实例。 + # write_to_movie: 是否将场景保存为视频文件。 + # break_into_partial_movies: 是否将视频文件拆分为多个部分。 + # save_pngs: 是否保存场景的每一帧为PNG格式图片。 + # png_mode: PNG图片的模式。 + # save_last_frame: 是否保存最后一帧。 + # movie_file_extension: 视频文件的扩展名。 + # input_file_path: 生成场景的Python文件路径。 + # output_directory: 输出目录。 + # file_name: 文件名。 + # subdirectory_for_videos: 是否为视频文件创建子目录。 + # open_file_upon_completion: 完成后是否打开文件。 + # show_file_location_upon_completion: 是否在完成时显示文件位置。 + # quiet: 是否安静模式,不输出信息。 + # total_frames: 总帧数。 + # progress_description_len: 进度描述的长度。 + # video_codec: 视频编解码器。 + # pixel_format: 像素格式。 + # saturation: 饱和度。 + # gamma: 伽马值。 self.scene: Scene = scene self.write_to_movie = write_to_movie self.break_into_partial_movies = break_into_partial_movies @@ -74,11 +94,16 @@ def __init__( self.saturation = saturation self.gamma = gamma - # State during file writing + # 文件写入时的状态 + # 处理子进程的管道 self.writing_process: sp.Popen | None = None + # 显示进度 self.progress_display: ProgressDisplay | None = None + # 场景是否因为中断结束,默认不会 self.ended_with_interrupt: bool = False + # 初始化输出目录 self.init_output_directories() + # 初始化音频 self.init_audio() # Output directories and files @@ -164,6 +189,8 @@ def get_saved_mobject_path(self, mobject: Mobject) -> str | None: POSIX path of chosenfile """, ] + + process = sp.Popen(cmds, stdout=sp.PIPE) file_path = process.stdout.read().decode("utf-8").split("\n")[0] if not file_path: From 49a32bf8bc0c8f38e19951f25c87c1f5a936d39a Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Thu, 15 Feb 2024 22:30:54 +0800 Subject: [PATCH 06/26] =?UTF-8?q?Create=20manimlib=E7=9B=AE=E5=BD=95?= =?UTF-8?q?=E7=BB=93=E6=9E=84.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\345\275\225\347\273\223\346\236\204.md" | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 "manimlib/manimlib\347\233\256\345\275\225\347\273\223\346\236\204.md" diff --git "a/manimlib/manimlib\347\233\256\345\275\225\347\273\223\346\236\204.md" "b/manimlib/manimlib\347\233\256\345\275\225\347\273\223\346\236\204.md" new file mode 100644 index 0000000000..28cc36bf11 --- /dev/null +++ "b/manimlib/manimlib\347\233\256\345\275\225\347\273\223\346\236\204.md" @@ -0,0 +1,110 @@ +manimlib/ # manim库 +├── __init__.py +├── __main__.py +├── default_config.yml # 默认配置文件 +├── config.py # 在这里处理命令传入的参数 +├── constants.py # 一些定义的常量 +├── extract_scene.py # 提取、运行场景 +├── shader_wrapper.py # Shader 包装器 +├── window.py # 反馈窗口 +├── logger.py # 创建 Logger 实例 +├── tex_templates/ # LaTeX模板 +│ ├── tex_templates.tex # tex 模板(将使用 latex 编译,默认) +│ └── ctex_templates.tex # 支持中文的 tex 模板(将使用 xelatex 编译) +├── camera/ # 相机 +│ └── camera.py # 包括 Camera 和 CameraFrame +├── scene/ # 场景 +│ ├── scene_file_writer.py # 用于将 scene 写入视频文件 +│ ├── scene.py # 最普通的场景 +│ ├── three_d_scene.py # 三维场景 +│ ├── sample_space_scene.py # 概率相关的样本空间场景 +│ └── vector_space_scene.py # 向量场场景 +├── animation/ # 动画 +│ ├── animation.py # 动画的基类 +│ ├── composition.py # 动画组 +│ ├── creation.py # 和 Create 有关的动画 +│ ├── fading.py # 和 Fade 有关的动画 +│ ├── growing.py # 和 Grow 有关的动画 +│ ├── indication.py # 一些用于强调的动画 +│ ├── movement.py # 和移动有关的动画 +│ ├── numbers.py # 实现对 DecimalNumber 数字的变化 +│ ├── rotation.py # 和旋转有关的动画 +│ ├── specialized.py # 一些针对特殊项目的不常用动画 +│ ├── transform_matching_parts.py # 自动匹配部分的 Transform +│ ├── transform.py # 一些 Transform 变换 +│ └── update.py # 从函数实现 update +├── mobject/ # 数学物品 +│ ├── mobject.py # 所有mobject的父类 +│ ├── types/ # 4种子类mobject +│ │ ├── dot_cloud.py # Dot cloud (PMobject 的一个子类) +│ │ ├── image_mobject.py # 插入图片 +│ │ ├── point_cloud_mobject.py # PMobject (点集构成的 mobject) +│ │ ├── surface.py # ParametricSurface +│ │ └── vectorized_mobject.py # VMobject (向量化的 mobject) +│ ├── svg/ # 和svg有关的mobject +│ │ ├── svg_mobject.py # SVGMobject +│ │ ├── brace.py # 大括号 +│ │ ├── drawings.py # svg 图像的一些特殊 mobject +│ │ ├── mtex_mobject.py # 依赖 LaTeX 实现的文字 +│ │ ├── tex_mobject.py # 依赖 LaTeX 实现的文字 +│ │ └── text_mobject.py # 依赖 manimpango 实现的文字 +│ ├── changing.py # 动态变化的 mobject +│ ├── coordinate_systems.py # 坐标系统 +│ ├── frame.py # 和 frame 有关的 mobject +│ ├── functions.py # 参数方程 +│ ├── geometry.py # 几何图形的 mobject +│ ├── interactive.py # 可交互的物件 +│ ├── matrix.py # 矩阵 +│ ├── mobject_update_utils.py # 一些定义的更新程序 +│ ├── number_line.py # 数轴 +│ ├── numbers.py # 可以变化的数字 +│ ├── probability.py # 和概率有关的 mobject +│ ├── shape_matchers.py # 适应其它物体大小的 mobject +│ ├── three_dimensions.py # 三维物体 +│ ├── value_tracker.py # ValueTracker(存储数的 mobject) +│ └── vector_field.py # 向量场 +├── event_handler/ # 处理交互事件(不常用) +│ ├── event_dispatcher.py +│ ├── event_listener.py +│ └── event_type.py +├── once_useful_constructs/ # 3b1b 为某些视频写的常用场景 +│ └── ... +├── shaders/ # 渲染中使用的GLSL脚本 +│ ├── simple_vert.glsl # 一个简单的对位置的 glsl 脚本 +│ ├── insert/ # 需要插入到其他脚本中的脚本片段 +│ │ ├── NOTE.md # 描述了如何插入脚本 +│ │ └── ... # 一些常用的脚本 +│ ├── image/ # 针对图像的 glsl +│ │ └── ... # 包含 vertex shader 和 fragment shader +│ ├── quadratic_bezier_fill/ # 针对二阶贝塞尔填充的 glsl +│ │ └── ... # 包含 vertex shader、fragment shader 和 geometry shader +│ ├── quadratic_bezier_stroke/ # 针对二阶贝塞尔线条的g lsl +│ │ └── ... # 包含 vertex shader、fragment shader 和 geometry shader +│ ├── mandelbrot_fractal/ # 针对M集的glsl +│ │ └── ... # 包含 vertex shader 和 fragment shader +│ ├── newton_fractal/ # 针对牛顿分形的 glsl +│ │ └── ... # 包含 vertex shader 和 fragment shader +│ ├── surface/ # 针对三维面的 glsl +│ │ └── ... # 包含 vertex shader 和 fragment shader +│ ├── textured_surface/ # 针对纹理面的 glsl +│ │ └── ... # 包含 vertex shader 和 fragment shader +│ └── true_dot/ # 对于一个点的 glsl +│ └── ... # 包含 vertex shader、fragment shader 和 geometry shader +└── utils/ # 一些实用的工具函数 + ├── bezier.py # 贝塞尔曲线 + ├── color.py # 颜色 + ├── config_ops.py # 处理 CONFIG + ├── customization.py # 读取 custom_config.yml + ├── debug.py # 在程序中 debug 的函数 + ├── directories.py # 读取配置文件中目录相关内容 + ├── family_ops.py # 处理 family 成员 + ├── file_ops.py # 处理文件目录 + ├── images.py # 读取图片 + ├── init_config.py # 自动配置指南 + ├── iterables.py # 和列表字典处理有关的函数 + ├── paths.py # 路径 + ├── rate_functions.py # 一些定义的 rate_function + ├── simple_functions.py # 一些常用函数 + ├── sounds.py # 处理声音 + ├── space_ops.py # 空间坐标计算 + └── tex_file_writing.py # 将字符串利用 LaTeX 写成 svg From 6c990170fd835ef8f5fad2f9f16fee1d0d163225 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:22:55 +0800 Subject: [PATCH 07/26] Update scene.py --- manimlib/scene/scene.py | 130 ++++++++++++++++++++++++---------------- 1 file changed, 78 insertions(+), 52 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 7bd7305fa6..2771aa59f4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -43,18 +43,14 @@ from typing import TYPE_CHECKING +# 在静态类型检查时导入所需的模块和类型 if TYPE_CHECKING: from typing import Callable, Iterable from manimlib.typing import Vect3 - from PIL.Image import Image - from manimlib.animation.animation import Animation -''' -typing模块中的Callable、Iterable和Vect3类型以及PIL.Image模块中的Image类 -和manimlib.animation.animation模块中的Animation类只在类型检查时才会被导入, -而在实际运行时不会被引入,从而避免了循环导入问题 -''' + + # 3D场景平移操作 PAN_3D_KEY = 'd' # 切换场景帧 @@ -90,36 +86,40 @@ class Scene(object): # 创建一个Scene场景对象,该对象包含了一系列配置参数,用于控制动画的播放和展示效果。 def __init__( self, - # camera_config:摄像头配置参数,是一个字典,默认为空字典。 - # file_writer_config:文件写入器配置参数,是一个字典,默认为空字典。 + # 窗口配置参数,默认为空字典 window_config: dict = dict(), + # 摄像头配置参数,默认为空字典 camera_config: dict = dict(), + # 文件写入器配置参数,默认为空字典 file_writer_config: dict = dict(), + # 是否跳过动画,默认为False skip_animations: bool = False, + # 是否始终更新对象,默认为False always_update_mobjects: bool = False, + # 开始动画的编号,默认为None start_at_animation_number: int | None = None, + # 结束动画的编号,默认为None end_at_animation_number: int | None = None, + # 是否保留进度条,默认为False leave_progress_bars: bool = False, + # 是否预览,默认为True preview: bool = True, + # 演示者模式,默认为False presenter_mode: bool = False, + # 是否显示动画进度,默认为False show_animation_progress: bool = False, + # 嵌入式异常模式,默认为空字符串 embed_exception_mode: str = "", + # 是否嵌入错误声音,默认为False embed_error_sound: bool = False, ): - # 将默认相机配置和传入的相机配置合并,确保所有参数都被正确设置 - self.camera_config = {**self.default_camera_config, **camera_config} - # 将默认窗口配置和传入的窗口配置合并,以便在创建窗口时使用这些配置 + # 将默认窗口配置和传入的窗口配置合并 self.window_config = {**self.default_window_config, **window_config} - # skip_animations:是否跳过动画,默认为False。 - # always_update_mobjects:是否始终更新对象,默认为False。 - # start_at_animation_number:开始动画的编号,默认为None。 - # end_at_animation_number:结束动画的编号,默认为None。 - # leave_progress_bars:是否保留进度条,默认为False。 - # preview:是否预览,默认为True。 - # presenter_mode:演示者模式,默认为False。 - # show_animation_progress:是否显示动画进度,默认为False。 - # embed_exception_mode:嵌入式异常模式,默认为空字符串。 - # embed_error_sound:是否嵌入错误声音,默认为False。 + # 将默认相机配置和传入的相机配置合并 + self.camera_config = {**self.default_camera_config, **camera_config} + # 将默认文件写入器配置和传入的文件写入器配置合并 + self.file_writer_config = {**self.default_file_writer_config, **file_writer_config} + # 依次代入各项参数 self.skip_animations = skip_animations self.always_update_mobjects = always_update_mobjects self.start_at_animation_number = start_at_animation_number @@ -135,8 +135,6 @@ def __init__( for config in self.camera_config, self.window_config: # 控制渲染时的采样级别,影响渲染质量 config["samples"] = self.samples - # 将文件写入器配置更新为默认文件写入器配置和传入的文件写入器配置的组合。这里使用了字典解包(**)的方式将两个配置合并 - self.file_writer_config = {**self.default_file_writer_config, **file_writer_config} # 是否需要预览场景 if self.preview: @@ -163,25 +161,25 @@ def __init__( self.frame.reorient(*self.default_frame_orientation) # 将当前方向设置为默认方向 self.frame.make_orientation_default() - # file_writer:文件写入器,用于将场景渲染为视频或图片。 - # mobjects:场景中的物体列表,包括摄像头帧和其他添加的物体。 - # render_groups:渲染组,用于控制物体的渲染顺序和分组。 - # id_to_mobject_map:物体ID映射表,记录了物体和其对应的ID的关系。 - # num_plays:场景播放次数计数器。 - # time:场景播放的时间。 - # skip_time:跳过动画的时间。 - # original_skipping_status:初始跳过动画的状态。 - # checkpoint_states:场景状态检查点,记录了场景的历史状态。 + # 文件写入器,用于将场景渲染为视频或图片。 self.file_writer = SceneFileWriter(self, **self.file_writer_config) + # 场景中的物体列表,包括摄像头帧和其他添加的物体 self.mobjects: list[Mobject] = [self.camera.frame] + # 渲染组,用于控制物体的渲染顺序和分组 self.render_groups: list[Mobject] = [] + # 物体ID映射表,记录了物体和其对应的ID的关系 self.id_to_mobject_map: dict[int, Mobject] = dict() + # 场景播放次数计数器 self.num_plays: int = 0 + # 场景播放的时间 self.time: float = 0 + # 跳过动画的时间 self.skip_time: float = 0 + # 初始跳过动画的状态 self.original_skipping_status: bool = self.skip_animations + # 场景状态检查点,记录场景的历史状态 self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict() - # undo_stack和redo_stack:撤销和重做栈,用于撤销和重做操作。 + # 撤销和重做栈,用于撤销和重做操作 self.undo_stack = [] self.redo_stack = [] @@ -207,48 +205,58 @@ def __init__( random.seed(self.random_seed) np.random.seed(self.random_seed) - # 返回了场景类的类名 + # 获取场景类名 def __str__(self) -> str: return self.__class__.__name__ + # 运行动画 def run(self) -> None: + # 动画开始的虚拟时间 self.virtual_animation_start_time: float = 0 + # 动画开始的真实时间 self.real_animation_start_time: float = time.time() + # 开始写入动画的帧到文件中,生成视频文件 self.file_writer.begin() - + # 准备动画的执行环境 self.setup() + # 监视异常 try: + # 构建动画 self.construct() + # 交互响应 self.interact() + # 结束场景,跳过 except EndScene: pass + # 键盘中断键 except KeyboardInterrupt: - # Get rid keyboard interupt symbols + # 清除中断键内容(如清除'^C'字样) print("", end="\r") + # 动画以键盘中断的方式结束 self.file_writer.ended_with_interrupt = True + # 执行动画结束的清理工作,如释放资源或关闭文件 self.tear_down() + # 接口方法,必须由子类重写 def setup(self) -> None: - """ - This is meant to be implement by any scenes which - are comonly subclassed, and have some common setup - involved before the construct method is called. - """ pass - + + # 所有动画发生的地方,也是个接口方法 def construct(self) -> None: - # Where all the animation happens - # To be implemented in subclasses pass - + + # 结束后的清理工作 def tear_down(self) -> None: + # 停止跳过动画 self.stop_skipping() + # 完成文件写入操作 self.file_writer.finish() + # 如果窗口存在,则销毁窗口并释放引用,以便回收 if self.window: self.window.destroy() self.window = None - # 与场景进行交互 + # 场景交互 def interact(self) -> None: # 如果没有窗口则直接返回 if self.window is None: @@ -266,35 +274,53 @@ def interact(self) -> None: # 更新帧,参数为每秒帧数的倒数 self.update_frame(1 / self.camera.fps) + # 嵌入动画 def embed( self, + # 在退出时是否关闭场景,默认要关闭 close_scene_on_exit: bool = True, + # 是否显示动画进度,默认不显示 show_animation_progress: bool = False, ) -> None: + # 只有在有预览时才需要嵌入动画 if not self.preview: - return # Embed is only relevant with a preview + # 如果不预览,跳过 + return + # 停止跳过动画设置 self.stop_skipping() + # 更新当前帧的内容 self.update_frame() + # 保存当前动画状态 self.save_state() + # 控制是否显示动画进度 self.show_animation_progress = show_animation_progress - # Create embedded IPython terminal to be configured + # 创建一个嵌入式的IPython终端 shell = InteractiveShellEmbed.instance() - - # Use the locals namespace of the caller + # 获取当前函数调用栈的前一帧 caller_frame = inspect.currentframe().f_back + # 将调用者的局部命名空间转换为字典,以便在嵌入的IPython终端中使用 local_ns = dict(caller_frame.f_locals) - # Add a few custom shortcuts + # 将一些自定义的快捷方式添加到了嵌入的IPython终端中 local_ns.update( + # 播放动画 play=self.play, + # 等待一段时间 wait=self.wait, + # 向场景中添加物体 add=self.add, + # 从场景中移除物体 remove=self.remove, + # 清空场景 clear=self.clear, + # 保存场景的状态 save_state=self.save_state, + # 撤销操作 undo=self.undo, + # 重做操作 redo=self.redo, + # i2g=self.i2g, i2m=self.i2m, checkpoint_paste=self.checkpoint_paste, From 91b686fc91bdf6337f98053c3df13fe02b45f613 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Fri, 16 Feb 2024 16:25:00 +0800 Subject: [PATCH 08/26] Update scene.py --- manimlib/scene/scene.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 2771aa59f4..b160d22143 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -320,8 +320,9 @@ def embed( undo=self.undo, # 重做操作 redo=self.redo, - # + # ID转Group操作 i2g=self.i2g, + # ID转Mobject操作 i2m=self.i2m, checkpoint_paste=self.checkpoint_paste, ) @@ -599,10 +600,11 @@ def ids_to_group(self, *id_values): lambda x: x is not None, map(self.id_to_mobject, id_values) )) - + + # 将传入的多个ID值作为参数,转换为一个组对象(Group) def i2g(self, *id_values): return self.ids_to_group(*id_values) - + # 将传入的ID值转换为一个物体对象(Mobject) def i2m(self, id_value): return self.id_to_mobject(id_value) From d4d9ee69b126324ca910a254e87395590ef8999a Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:05:49 +0800 Subject: [PATCH 09/26] Update scene.py --- manimlib/scene/scene.py | 95 ++++++++++++++++++++++++++++++----------- 1 file changed, 70 insertions(+), 25 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index b160d22143..af31dd42e7 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -424,30 +424,41 @@ def emit_frame(self) -> None: if not self.skip_animations: self.file_writer.write_frame(self.camera) - # Related to updating + # 更新相关 + # 更新场景中的所有物体 def update_mobjects(self, dt: float) -> None: + # 遍历场景中的每个物体 for mobject in self.mobjects: + # 传递时间增量实现物体的更新 mobject.update(dt) + # 检查是否应该更新物体 def should_update_mobjects(self) -> bool: + # 如果要总是更新,则更新 return self.always_update_mobjects or any([ + # 否则,检查场景中每个物体是否有与时间相关的更新器 len(mob.get_family_updaters()) > 0 + # 如果有任何一个物体有时间相关的更新器,则更新,否则不更新 for mob in self.mobjects ]) + # 检查场景中是否有与时间相关的更新器 def has_time_based_updaters(self) -> bool: return any([ + # 遍历场景中的每个物体及其子物体,检查是否有任何一个更新器与时间相关 sm.has_time_based_updater() for mob in self.mobjects() for sm in mob.get_family() ]) - # Related to time + # 时间相关 + # 获取场景当前时间 def get_time(self) -> float: return self.time + # 实现对时间的增量操作 def increment_time(self, dt: float) -> None: self.time += dt @@ -591,13 +602,18 @@ def get_group(self, *mobjects): return VGroup(*mobjects) else: return Group(*mobjects) - + + # 根据一个ID值返回所对应的物体对象 def id_to_mobject(self, id_value): return self.id_to_mobject_map[id_value] + # 根据多个ID值返回所对应的多个物体对象所组成的组对象 def ids_to_group(self, *id_values): + # 遍历ID列表中的每个ID值 return self.get_group(*filter( + # 如果ID值不为None lambda x: x is not None, + # 则将对应的物体对象添加到mobjects列表中,并将mobjects列表中的物体对象作为参数传递给get_group方法,以创建组对象 map(self.id_to_mobject, id_values) )) @@ -608,8 +624,9 @@ def i2g(self, *id_values): def i2m(self, id_value): return self.id_to_mobject(id_value) - # Related to skipping + # 跳过相关 + # def update_skipping_status(self) -> None: if self.start_at_animation_number is not None: if self.num_plays == self.start_at_animation_number: @@ -624,8 +641,9 @@ def stop_skipping(self) -> None: self.virtual_animation_start_time = self.time self.skip_animations = False - # Methods associated with running animations + # 运行动画相关 + # def get_time_progression( self, run_time: float, @@ -832,79 +850,106 @@ def save_state(self) -> None: if len(self.undo_stack) > self.max_num_saved_states: self.undo_stack.pop(0) + # 撤销操作 def undo(self): if self.undo_stack: self.redo_stack.append(self.get_state()) self.restore_state(self.undo_stack.pop()) + # 重做操作 def redo(self): if self.redo_stack: self.undo_stack.append(self.get_state()) self.restore_state(self.redo_stack.pop()) + # 在交互式开发过程中运行(或重新运行)一个场景代码块 def checkpoint_paste( self, + # 默认不跳过 skip: bool = False, + # 默认不记录 record: bool = False, + # 默认加进度条 progress_bar: bool = True ): - """ - Used during interactive development to run (or re-run) - a block of scene code. - - If the copied selection starts with a comment, this will - revert to the state of the scene the first time this function - was called on a block of code starting with that comment. - """ + # 获取当前的IPython shell对象 shell = get_ipython() + # 如果shell或window为空,则抛出异常,表示无法在非IPython shell环境或没有窗口的情况下调用此方法 if shell is None or self.window is None: raise Exception( - "Scene.checkpoint_paste cannot be called outside of " + - "an ipython shell" + "Scene.checkpoint_paste cannot be called outside of an ipython shell" ) - + # 从剪贴板中获取内容 pasted = pyperclip.paste() + # 获取pasted的第一行内容,并将其存储在变量line0中。lstrip()用于删除字符串开头的空格和换行符 line0 = pasted.lstrip().split("\n")[0] + # 如果第一行内容以#开头 if line0.startswith("#"): + # 如果以#开头的第一行内容不在字典中 if line0 not in self.checkpoint_states: + # 则将以该行内容为键的检查点状态保存到self.checkpoint_states字典中 self.checkpoint(line0) else: + # 否则将场景恢复到以该行内容为键的检查点状态 self.revert_to_checkpoint(line0) - + + # 保存当前的跳过动画设置 prev_skipping = self.skip_animations + # 跳过动画设置为skip指定的值 self.skip_animations = skip - + # 保存当前的显示动画进度设置 prev_progress = self.show_animation_progress + # 显示动画进度设置为progress_bar指定的值 self.show_animation_progress = progress_bar - + + # 如果有记录 if record: + # 禁用窗口帧缓冲区 self.camera.use_window_fbo(False) + # 开始录制动画 self.file_writer.begin_insert() - + + # 在IPython shell中运行剪贴板中的代码块 shell.run_cell(pasted) - + + # 如果有记录 if record: + # 结束录制动画 self.file_writer.end_insert() + # 重新启用窗口帧缓冲区 self.camera.use_window_fbo(True) - + + # 恢复先前保存的跳过动画设置 self.skip_animations = prev_skipping + # 恢复先前保存的显示动画进度设置 self.show_animation_progress = prev_progress + # 将当前场景状态保存为一个检查点 def checkpoint(self, key: str): + # 以key为键,以当前场景状态为值 self.checkpoint_states[key] = self.get_state() - + + # 将场景恢复到之前保存的某个检查点的状态 def revert_to_checkpoint(self, key: str): + # 若key不存在,即没有找到对应的检查点 if key not in self.checkpoint_states: + # 报错 log.error(f"No checkpoint at {key}") return + # 将self.checkpoint_states字典中所有的键(即检查点的标识)转换为列表 all_keys = list(self.checkpoint_states.keys()) + # 获取key在列表中的索引,即找到指定检查点在所有检查点中的位置 index = all_keys.index(key) + # 遍历all_keys列表中从index+1开始的所有元素,这些元素表示比指定检查点更新的检查点 for later_key in all_keys[index + 1:]: + # 移除比指定检查点更新的所有检查点,保留指定检查点及之前的检查点 self.checkpoint_states.pop(later_key) - + # 将场景恢复到指定检查点的状态 self.restore_state(self.checkpoint_states[key]) - + + # 清除所有的检查点 def clear_checkpoints(self): + # 将self.checkpoint_states字典清空,重新开始记录新的检查点 self.checkpoint_states = dict() def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None: From 0cfd6073b8288c549d97d12705c8b7fcc2dfc86f Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Fri, 16 Feb 2024 17:41:10 +0800 Subject: [PATCH 10/26] Update mobject.py --- manimlib/mobject/mobject.py | 73 +++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 11 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index bca9302667..b7b698709d 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -57,33 +57,43 @@ NonTimeUpdater = Callable[["Mobject"], "Mobject" | None] Updater = Union[TimeBasedUpdater, NonTimeUpdater] - +# 数学对象类 class Mobject(object): - """ - Mathematical Object - """ + # 数学对象的维度,默认为3维 dim: int = 3 + # 用于此数学对象的着色器文件夹的路径,默认为空字符串 shader_folder: str = "" + # 渲染时使用的图元类型 render_primitive: int = moderngl.TRIANGLE_STRIP - # Must match in attributes of vert shader + # 顶点着色器的数据类型,必须与垂直着色器的属性匹配 shader_dtype: np.dtype = np.dtype([ + # 顶点的位置(三维坐标) ('point', np.float32, (3,)), + # 颜色(四维RGBA值) ('rgba', np.float32, (4,)), ]) + # 对齐数据键 aligned_data_keys = ['point'] + # 点类似数据键 pointlike_data_keys = ['point'] + # 数学对象类的初始化方法 def __init__( self, + # 颜色,默认为WHITE color: ManimColor = WHITE, + # 不透明度,默认为1.0 opacity: float = 1.0, + # 阴影颜色,默认为(0.0, 0.0, 0.0) shading: Tuple[float, float, float] = (0.0, 0.0, 0.0), - # For shaders + # 纹理路径,不同部分使用不同的纹理,默认为空字典 texture_paths: dict[str, str] | None = None, - # If true, the mobject will not get rotated according to camera position + # 是否固定在帧中,默认不会随着相机位置的旋转而旋转 is_fixed_in_frame: bool = False, + # 是否进行深度测试,默认不进行 depth_test: bool = False, ): + # 依次代入各项参数 self.color = color self.opacity = opacity self.shading = shading @@ -91,70 +101,111 @@ def __init__( self._is_fixed_in_frame = is_fixed_in_frame self.depth_test = depth_test - # Internal state + # 内部状态 + # 当前对象的所有子对象,默认为空字典 self.submobjects: list[Mobject] = [] + # 当前对象的所有父对象,默认为空字典 self.parents: list[Mobject] = [] + # 当前对象及其所有相关对象组成的对象组,默认为只有自己的字典 self.family: list[Mobject] = [self] + # 锁定的数据键集合,表示不能修改的数据键,默认为空集合 self.locked_data_keys: set[str] = set() + # 常量数据键集合,表示不会在运行时更改的数据键,默认为空集合 self.const_data_keys: set[str] = set() + # 锁定的统一键集合,表示不能修改的统一键,默认为空集合 self.locked_uniform_keys: set[str] = set() + # 是否需要计算新的边界框,默认需要 self.needs_new_bounding_box: bool = True + # 对象是否正在进行动画,默认没有 self._is_animating: bool = False + # 保存的对象状态,默认还没东西 self.saved_state = None + # 对象的目标状态,默认还没东西 self.target = None + # 对象的边界框,默认为3×3的零矩阵 self.bounding_box: Vect3Array = np.zeros((3, 3)) + # 着色器是否已初始化,默认还没有 self._shaders_initialized: bool = False + # 数据是否已更改,默认已更改 self._data_has_changed: bool = True + # 着色器代码替换字典,用于替换着色器中的代码,默认为空字典 self.shader_code_replacements: dict[str, str] = dict() + # 初始化对象的数据 self.init_data() + # 初始化对象的默认数据为一个值为1的数组,数组长度为1,数据类型与对象的数据类型一致 self._data_defaults = np.ones(1, dtype=self.data.dtype) + # 初始化对象的统一键 self.init_uniforms() + # 初始化对象的更新器 self.init_updaters() + # 初始化对象的事件监听器 self.init_event_listners() + # 初始化对象的点 self.init_points() + # 初始化对象的颜色 self.init_colors() + # 如果需要深度测试 if self.depth_test: + # 就进行深度测试 self.apply_depth_test() + # 获取对象的类名 def __str__(self): return self.__class__.__name__ + # 对象之间的合并或组合 def __add__(self, other: Mobject) -> Mobject: assert(isinstance(other, Mobject)) + # 创建一个新的群组对象,包含对象自身和新加入的对象 return self.get_group_class()(self, other) + # 对象自身的复制或重复 def __mul__(self, other: int) -> Mobject: assert(isinstance(other, int)) + # 根据整数多少复制几个对象 return self.replicate(other) + # 初始化对象的数据 def init_data(self, length: int = 0): + # 创建一个指定长度的全零数组 self.data = np.zeros(length, dtype=self.shader_dtype) + # 初始化对象的统一键 def init_uniforms(self): self.uniforms: UniformDict = { + # 是否固定在帧中 "is_fixed_in_frame": float(self._is_fixed_in_frame), + # 对象的阴影效果 "shading": np.array(self.shading, dtype=float), } - + + # 初始化对象的颜色 def init_colors(self): + # 根据对象的颜色和不透明度设置其颜色 self.set_color(self.color, self.opacity) + # 初始化对象的点,子类中实现 def init_points(self): - # Typically implemented in subclass, unlpess purposefully left blank pass + # 设置对象的统一键 def set_uniforms(self, uniforms: dict) -> Self: + # 遍历uniforms字典的每个键值对 for key, value in uniforms.items(): + # 如果值是NumPy数组 if isinstance(value, np.ndarray): + # 创建其副本以避免修改原数组 value = value.copy() + # 将键值对添加到统一键键值对属性中 self.uniforms[key] = value return self + # @property用于将一个方法转换为属性,使得可以像访问属性一样访问该方法 @property + # 构建对象的动画 def animate(self) -> _AnimationBuilder: - # Borrowed from https://github.com/ManimCommunity/manim/ return _AnimationBuilder(self) def note_changed_data(self, recurse_up: bool = True) -> Self: From 38d6ceeb45dfa17ed99c6438fcf76d2643dbacde Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Fri, 16 Feb 2024 18:54:11 +0800 Subject: [PATCH 11/26] Update mobject.py --- manimlib/mobject/mobject.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index b7b698709d..d53beb7282 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -2220,15 +2220,23 @@ def set_location(self, new_loc: npt.ArrayLike) -> Self: self.set_points(np.array(new_loc, ndmin=2, dtype=float)) return self - +# 动画构造器类 class _AnimationBuilder: + # 动画构造器类的构造方法 def __init__(self, mobject: Mobject): + # 保存传入的Mobject对象 self.mobject = mobject + # 保存要覆盖的动画,默认为空 self.overridden_animation = None + # 生成动画的目标状态,用于后续动画效果的计算 self.mobject.generate_target() + # 标记是否正在链式调用动画方法,默认没有 self.is_chaining = False + # 保存要调用的动画方法,默认为空列表 self.methods: list[Callable] = [] + # 保存动画方法的参数,默认为空字典 self.anim_args = {} + # 标记是否可以传递参数给动画方法,默认可以 self.can_pass_args = True def __getattr__(self, method_name: str): @@ -2259,16 +2267,14 @@ def __call__(self, **kwargs): def set_anim_args(self, **kwargs): ''' - You can change the args of :class:`~manimlib.animation.transform.Transform`, such as - + 可以更改 :class:`~manimlib.animation.transform.Transform` 的参数,比如: - ``run_time`` - ``time_span`` - ``rate_func`` - ``lag_ratio`` - ``path_arc`` - ``path_func`` - - and so on. + 等等 ''' if not self.can_pass_args: From 3d12c3740b6ee1d2e1be7cd8e353ed7629a531f8 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Fri, 16 Feb 2024 22:11:25 +0800 Subject: [PATCH 12/26] Update mobject.py --- manimlib/mobject/mobject.py | 260 ++++++++++++++++++++++++++++-------- 1 file changed, 205 insertions(+), 55 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index d53beb7282..d3d9373d96 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -518,33 +518,53 @@ def digest_mobject_attrs(self) -> Self: self.set_submobjects(list_update(self.submobjects, mobject_attrs)) return self - # Submobject organization + # 子对象组织 + # 排列 def arrange( self, + # 子对象排列的方向,默认为右方向 direction: Vect3 = RIGHT, + # 是否居中排列,默认居中排列 center: bool = True, + # 其他参数 **kwargs ) -> Self: + # 遍历子对象 for m1, m2 in zip(self.submobjects, self.submobjects[1:]): + # 子对象的下一个子对象 m2.next_to(m1, direction, **kwargs) + # 如果需要居中 if center: + # 居中 self.center() return self + # 将子对象以网格形式排列 def arrange_in_grid( self, + # 网格的行数,默认为None n_rows: int | None = None, + # 网格的列数,默认为None n_cols: int | None = None, + # 子对象之间的间距,默认为None buff: float | None = None, + # 子对象水平方向的间距,默认为None h_buff: float | None = None, + # 子对象垂直方向的间距,默认为None v_buff: float | None = None, + # 子对象之间的间距比例,默认为None buff_ratio: float | None = None, + # 子对象水平方向的间距比例,默认为0.5 h_buff_ratio: float = 0.5, + # 子对象垂直方向的间距比例,默认为0.5 v_buff_ratio: float = 0.5, + # 子对象的对齐方向,默认为ORIGIN aligned_edge: Vect3 = ORIGIN, + # 是否填充网格,默认为True,先填满行再填充列 fill_rows_first: bool = True ) -> Self: + submobs = self.submobjects if n_rows is None and n_cols is None: n_rows = int(np.sqrt(len(submobs))) @@ -1075,103 +1095,168 @@ def wag( )) return self - # Positioning methods + # 方向与定位 + # 将物体移动到场景的中心位置 def center(self) -> Self: + # 获取物体的中心位置,将物体向其相反方向平移,使其移动到场景的中心位置 self.shift(-self.get_center()) return self + # 将物体沿着指定方向对齐到边界 def align_on_border( self, + # 指定对齐的方向向量 direction: Vect3, + # 指定对齐时的间距,默认采用默认间距 buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER ) -> Self: - """ - Direction just needs to be a vector pointing towards side or - corner in the 2d plane. - """ + # “方向”只需要是一个指向二维平面上某一侧面或角落的向量即可 + # 计算目标点 target_point = np.sign(direction) * (FRAME_X_RADIUS, FRAME_Y_RADIUS, 0) + # 获取物体边界框上指定方向上的点 point_to_align = self.get_bounding_box_point(direction) + # 计算需要平移的距离 shift_val = target_point - point_to_align - buff * np.array(direction) + # 根据direction的符号调整平移向量的方向 shift_val = shift_val * abs(np.sign(direction)) + # 将物体按计算得到的平移向量移动 self.shift(shift_val) return self + # 将物体对齐到指定角落 def to_corner( self, + # 指定要对齐到的角落,默认为左下角 corner: Vect3 = LEFT + DOWN, + # 间距,默认为默认对象间距离 buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER ) -> Self: return self.align_on_border(corner, buff) + # 将物体对齐到指定边缘 def to_edge( self, + # 指定要对齐到的边缘,默认为左边缘 edge: Vect3 = LEFT, + # 指定对齐时的间距,默认为默认对象间距离 buff: float = DEFAULT_MOBJECT_TO_EDGE_BUFFER ) -> Self: return self.align_on_border(edge, buff) + # 将对象移动到另一个对象 def next_to( self, + # 移动到的对象,一个对象或者一个点 mobject_or_point: Mobject | Vect3, + # 移动方向,默认向右 direction: Vect3 = RIGHT, + # 间距,采用默认对象间距离 buff: float = DEFAULT_MOBJECT_TO_MOBJECT_BUFFER, + # 对齐的边,默认为ORIGIN aligned_edge: Vect3 = ORIGIN, + # 传入一个子对象,默认为None submobject_to_align: Mobject | None = None, + # 传入一个子对象的索引,默认为None index_of_submobject_to_align: int | slice | None = None, + # 传入一个坐标掩码,默认为np.array([1, 1, 1]) coor_mask: Vect3 = np.array([1, 1, 1]), ) -> Self: + # 如果移动到的对象是Mobject对象 if isinstance(mobject_or_point, Mobject): + # 获取移动到的对象 mob = mobject_or_point + # 如果对齐子对象的索引不为空 if index_of_submobject_to_align is not None: + # 则获取目标对齐器 target_aligner = mob[index_of_submobject_to_align] else: + # 如果未提供索引,则使用整个Mobject作为目标对齐器 target_aligner = mob - target_point = target_aligner.get_bounding_box_point( - aligned_edge + direction - ) + # 根据对齐边缘和方向,获取目标对齐点 + target_point = target_aligner.get_bounding_box_point(aligned_edge + direction) + # 如果移动到的对象是坐标点,则直接将其作为目标点 else: target_point = mobject_or_point + # 确定对齐器 if submobject_to_align is not None: + # 如果提供了待对齐的子对象,则使用其作为对齐器 aligner = submobject_to_align elif index_of_submobject_to_align is not None: + # 如果未提供待对齐的子对象但提供了索引,则使用相应子对象作为对齐器 aligner = self[index_of_submobject_to_align] else: + # 如果既未提供待对齐的子对象也未提供索引,则使用整个Mobject作为对齐器 aligner = self + # 获取待对齐点,根据对齐边缘和方向对齐器进行计算得到 point_to_align = aligner.get_bounding_box_point(aligned_edge - direction) + # 移动Mobject,使目标点与待对齐点对齐,并考虑缓冲距离(buff * direction) self.shift((target_point - point_to_align + buff * direction) * coor_mask) return self + # 将物体移动到屏幕内部 def shift_onto_screen(self, **kwargs) -> Self: + # 定义了场景的长度和宽度 space_lengths = [FRAME_X_RADIUS, FRAME_Y_RADIUS] + # 遍历上、下、左、右四个方向 for vect in UP, DOWN, LEFT, RIGHT: + # 获取向量中绝对值最大的分量所在的维度 dim = np.argmax(np.abs(vect)) + # 获取缓冲区间距,如果未指定则使用默认值 buff = kwargs.get("buff", DEFAULT_MOBJECT_TO_EDGE_BUFFER) + # 计算在当前维度上可以移动的最大值 max_val = space_lengths[dim] - buff + # 获取沿着当前方向的边缘中心点 edge_center = self.get_edge_center(vect) + # 检查当前边缘中心点是否超出了屏幕范围 if np.dot(edge_center, vect) > max_val: + # 如果超出了屏幕范围,则将物体对齐到当前方向的边缘 self.to_edge(vect, **kwargs) return self + # 判断物体是否在超出屏幕范围 def is_off_screen(self) -> bool: + # 如果物体最左边的点的x坐标大于FRAME_X_RADIUS,则超出 if self.get_left()[0] > FRAME_X_RADIUS: return True + # 如果物体最右边的点的x坐标小于-FRAME_X_RADIUS,则超出 if self.get_right()[0] < -FRAME_X_RADIUS: return True + # 如果物体最下边的点的x坐标大于FRAME_Y_RADIUS,则超出 if self.get_bottom()[1] > FRAME_Y_RADIUS: return True + # 如果物体最上边的点的x坐标小于-FRAME_Y_RADIUS,则超出 if self.get_top()[1] < -FRAME_Y_RADIUS: return True + # 均不超出 return False - def stretch_about_point(self, factor: float, dim: int, point: Vect3) -> Self: + # 围绕指定点按照指定因子进行拉伸 + def stretch_about_point(self, + # 拉伸因子 + factor: float, + # 拉伸的维度 + dim: int, + # 围绕的点 + point: Vect3 + ) -> Self: return self.stretch(factor, dim, about_point=point) - def stretch_in_place(self, factor: float, dim: int) -> Self: - # Now redundant with stretch + # 在原地按照指定因子进行拉伸 + def stretch_in_place(self, + factor: float, + dim: int + ) -> Self: + # 目前已经被stretch方法取代 return self.stretch(factor, dim) - def rescale_to_fit(self, length: float, dim: int, stretch: bool = False, **kwargs) -> Self: + # 将物体按照指定维度的长度重新缩放到指定长度 + def rescale_to_fit(self, + length: float, + dim: int, + stretch: bool = False, + **kwargs + ) -> Self: old_length = self.length_over_dim(dim) if old_length == 0: return self @@ -2071,84 +2156,112 @@ def render(self, ctx: Context, camera_uniforms: dict): shader_wrapper.pre_render() shader_wrapper.render() - # Event Handlers - """ - Event handling follows the Event Bubbling model of DOM in javascript. - Return false to stop the event bubbling. - To learn more visit https://www.quirksmode.org/js/events_order.html - - Event Callback Argument is a callable function taking two arguments: - 1. Mobject - 2. EventData - """ + # 事件处理 + + # 该函数是一个事件处理函数,它遵循JavaScript中DOM的事件冒泡模型。 + # 它接受一个可调用函数作为回调参数。 + # 该函数接受两个参数:一个Mobject对象和一个EventData对象。 + # 如果回调函数返回false,则事件冒泡将停止。 + # 参见https://www.quirksmode.org/js/events_order.html + # 初始化事件监听器 def init_event_listners(self): + # 创建一个空的事件监听器列表 self.event_listners: list[EventListener] = [] + # 添加事件监听器 def add_event_listner( self, + # 事件类型 event_type: EventType, + # 事件回调函数 event_callback: Callable[[Mobject, dict[str]]] ): + # 创建一个事件监听器 event_listner = EventListener(self, event_type, event_callback) + # 添加到事件监听器列表中 self.event_listners.append(event_listner) + # 将事件监听器添加到事件分发器中 EVENT_DISPATCHER.add_listner(event_listner) return self + # 移除事件监听器 def remove_event_listner( self, + # 事件类型 event_type: EventType, + # 事件回调函数 event_callback: Callable[[Mobject, dict[str]]] ): + # 创建一个事件监听器 event_listner = EventListener(self, event_type, event_callback) + # 从事件监听器列表中移除事件监听器 while event_listner in self.event_listners: self.event_listners.remove(event_listner) + # EVENT_DISPATCHER.remove_listner(event_listner) return self + # 清空事件监听器 def clear_event_listners(self, recurse: bool = True): + # 清空事件监听器列表 self.event_listners = [] if recurse: for submob in self.submobjects: submob.clear_event_listners(recurse=recurse) return self + # 获取事件监听器 def get_event_listners(self): return self.event_listners + # 获取事件监听器的家族 def get_family_event_listners(self): return list(it.chain(*[sm.get_event_listners() for sm in self.get_family()])) + # 判断是否有事件监听器 def get_has_event_listner(self): + # 遍历事件监听器列表 return any( + # 判断是否有事件监听器 mob.get_event_listners() + # 遍历事件监听器的家族 for mob in self.get_family() ) + # 添加鼠标移动监听器 def add_mouse_motion_listner(self, callback): self.add_event_listner(EventType.MouseMotionEvent, callback) + # 移除鼠标移动监听器 def remove_mouse_motion_listner(self, callback): self.remove_event_listner(EventType.MouseMotionEvent, callback) + # 添加鼠标按下监听器 def add_mouse_press_listner(self, callback): self.add_event_listner(EventType.MousePressEvent, callback) + # 移除鼠标按下监听器 def remove_mouse_press_listner(self, callback): self.remove_event_listner(EventType.MousePressEvent, callback) + # 添加鼠标释放监听器 def add_mouse_release_listner(self, callback): self.add_event_listner(EventType.MouseReleaseEvent, callback) + # 移除鼠标释放监听器 def remove_mouse_release_listner(self, callback): self.remove_event_listner(EventType.MouseReleaseEvent, callback) + # 添加鼠标拖拽监听器 def add_mouse_drag_listner(self, callback): self.add_event_listner(EventType.MouseDragEvent, callback) + # 移除鼠标拖拽监听器 def remove_mouse_drag_listner(self, callback): self.remove_event_listner(EventType.MouseDragEvent, callback) + # def add_mouse_scroll_listner(self, callback): self.add_event_listner(EventType.MouseScrollEvent, callback) @@ -2176,46 +2289,67 @@ def throw_error_if_no_points(self): caller_name = sys._getframe(1).f_code.co_name raise Exception(message.format(caller_name)) - +# 组类 class Group(Mobject): + # 组类的构造方法 def __init__(self, *mobjects: Mobject, **kwargs): + # 检查所有子对象是否为数学对象 if not all([isinstance(m, Mobject) for m in mobjects]): - raise Exception("All submobjects must be of type Mobject") + # 抛出异常 + raise Exception("所有子对象必须为Mobject类型") + # 使用父类的构造方法 Mobject.__init__(self, **kwargs) + # 添加子对象 self.add(*mobjects) if any(m.is_fixed_in_frame() for m in mobjects): + # self.fix_in_frame() - + # 向当前组类中添加另一个数学对象或组对象 def __add__(self, other: Mobject | Group) -> Self: + # 判断外来者是否为数学对象 assert(isinstance(other, Mobject)) + # 返回当前组对象 return self.add(other) - +# 点类 class Point(Mobject): + # 点类的构造方法 def __init__( self, + # 点的坐标,默认为ORIGIN location: Vect3 = ORIGIN, + # 点的宽度,默认为1e-6 artificial_width: float = 1e-6, + # 点的高度,默认为1e-6 artificial_height: float = 1e-6, + # 其他关键字参数 **kwargs ): + # 点的宽度和高度 self.artificial_width = artificial_width self.artificial_height = artificial_height + # 调用父类Mobject的构造方法 super().__init__(**kwargs) + # 设置点的位置 self.set_location(location) + # 获取点的宽度 def get_width(self) -> float: return self.artificial_width + # 获取点的高度 def get_height(self) -> float: return self.artificial_height + # 获取点的位置,三维向量 def get_location(self) -> Vect3: return self.get_points()[0].copy() - + + # 获取包围盒的点,即返回点的位置,三维向量 def get_bounding_box_point(self, *args, **kwargs) -> Vect3: return self.get_location() - + + # 设置点的位置,即设置点的顶点坐标为new_loc def set_location(self, new_loc: npt.ArrayLike) -> Self: self.set_points(np.array(new_loc, ndmin=2, dtype=float)) return self @@ -2239,66 +2373,82 @@ def __init__(self, mobject: Mobject): # 标记是否可以传递参数给动画方法,默认可以 self.can_pass_args = True + # 在访问实例属性时动态地获取属性值,method_name 表示要访问的属性名 def __getattr__(self, method_name: str): + # 获取self.mobject.target对象的属性method_name method = getattr(self.mobject.target, method_name) + # 获取到的属性 method 添加到self.methods列表中 self.methods.append(method) + # 检查获取到的属性method是否具有_override_animate属性,如果有则将其赋值给has_overridden_animation has_overridden_animation = hasattr(method, "_override_animate") - + # 检查是否存在方法链和是否存在被覆盖的动画 if (self.is_chaining and has_overridden_animation) or self.overridden_animation: + # 如果是则抛出NotImplementedError异常 raise NotImplementedError( - "Method chaining is currently not supported for " + \ - "overridden animations" + "目前不支持被覆盖动画的方法链" ) - + # 更新动画的目标对象 def update_target(*method_args, **method_kwargs): + # 检查是否存在已被覆盖的动画 if has_overridden_animation: - self.overridden_animation = method._override_animate( - self.mobject, *method_args, **method_kwargs - ) + # 如果存在,将已被覆盖的动画添加到动画构造器 + self.overridden_animation = method._override_animate(self.mobject, *method_args, **method_kwargs) else: + # 如果不存在,调用原始方法,更新动画的属性或状态 method(*method_args, **method_kwargs) + # 返回动画构造器 return self - + # 方法链正在进行中 self.is_chaining = True + # 返回目标更新函数 return update_target + # 对象实例可以像函数一样被调用,接受任意关键字参数kwargs def __call__(self, **kwargs): + # 然后将这些关键字参数传递给set_anim_args方法,并返回对象实例本身 return self.set_anim_args(**kwargs) + # 设置动画构造器的动画参数 def set_anim_args(self, **kwargs): - ''' - 可以更改 :class:`~manimlib.animation.transform.Transform` 的参数,比如: - - ``run_time`` - - ``time_span`` - - ``rate_func`` - - ``lag_ratio`` - - ``path_arc`` - - ``path_func`` - 等等 - ''' - + # 可以更改 :class:`~manimlib.animation.transform.Transform` 的参数,比如: + # - ``run_time``(运行时间) + # - ``time_span``(时间跨度) + # - ``rate_func``(缓动函数,控制动画的速度变化) + # - ``lag_ratio``(延迟比例,用于控制动画的延迟) + # - ``path_arc``(路径弧度,用于指定动画对象在运动过程中的路径弧度) + # - ``path_func``(路径函数,用于控制动画对象在运动过程中的路径形状) + # 等等 + # 如果不允许传递参数,说明已经设置过动画参数, if not self.can_pass_args: + # 抛出ValueError异常 raise ValueError( - "Animation arguments can only be passed by calling ``animate`` " + \ - "or ``set_anim_args`` and can only be passed once", + "动画参数只能通过调用 'animate' 或 'set_anim_args' 来传递,且只能被传递一次" ) - + # 动画参数设置为传入的参数 self.anim_args = kwargs + # 已经设置过动画参数,不允许再次传递 self.can_pass_args = False + # 返回该动画构造器 return self + # 构建动画构造器 def build(self): from manimlib.animation.transform import _MethodAnimation - + # 如果存在被覆盖的动画对象,则返回该动画对象 if self.overridden_animation: return self.overridden_animation - + # 否则,根据动画构造器的各项信息创建一个新的动画对象 return _MethodAnimation(self.mobject, self.methods, **self.anim_args) - +# 通过使用@override_animate装饰器,可以在定义动画方法时指定一个重写的动画方法 +# 这样,在调用动画方法时,实际执行的将是重写的动画方法 +# 重写动画方法的装饰器函数 def override_animate(method): + # 装饰一个动画方法 def decorator(animation_method): + # 将该动画方法赋值给重写动画 method._override_animate = animation_method + # 返回动画方法 return animation_method - + # 返回用于装饰需要重写的动画方法 return decorator From c136c9618cb635f83832a99908da897787e8152e Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 10:17:35 +0800 Subject: [PATCH 13/26] Update __init__.py --- manimlib/event_handler/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/manimlib/event_handler/__init__.py b/manimlib/event_handler/__init__.py index c6c2553606..b106345bb6 100644 --- a/manimlib/event_handler/__init__.py +++ b/manimlib/event_handler/__init__.py @@ -1,6 +1,4 @@ from manimlib.event_handler.event_dispatcher import EventDispatcher - -# This is supposed to be a Singleton -# i.e., during runtime there should be only one object of Event Dispatcher +# 这应该是一个单例,即在运行期间应该只有一个事件调度对象 EVENT_DISPATCHER = EventDispatcher() From e258426c5d84f18d5ac78459af25f6db7a66af35 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 17:03:12 +0800 Subject: [PATCH 14/26] Update camera.py --- manimlib/camera/camera.py | 145 ++++++++++++++++++++++++++++++-------- 1 file changed, 115 insertions(+), 30 deletions(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index 08fe1abc88..27a5dcc4f5 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -21,33 +21,47 @@ from manimlib.typing import ManimColor, Vect3 from manimlib.window import Window - +# 相机类 class Camera(object): + # 相机类的构造方法 def __init__( self, + # 可选的窗口对象,默认为None window: Optional[Window] = None, + # 可选的背景图片路径,默认为None background_image: Optional[str] = None, + # 帧配置参数,默认为空字典 frame_config: dict = dict(), + # 帧的实际尺寸可能会根据像素的纵横比进行调整 + # 像素宽度,默认为DEFAULT_PIXEL_WIDTH pixel_width: int = DEFAULT_PIXEL_WIDTH, + # 像素高度,默认为DEFAULT_PIXEL_HEIGHT pixel_height: int = DEFAULT_PIXEL_HEIGHT, + # 帧率,即每秒帧数,默认为DEFAULT_FPS fps: int = DEFAULT_FPS, - # Note: frame height and width will be resized to match the pixel aspect ratio + # 背景颜色,默认为黑色 background_color: ManimColor = BLACK, + # 背景透明度,默认为1.0 background_opacity: float = 1.0, - # Points in vectorized mobjects with norm greater - # than this value will be rescaled. + # 当矢量化物体中的点的范数大于指定值时,这些点将被重新缩放 + # 最大允许范数,默认为FRAME_WIDTH max_allowable_norm: float = FRAME_WIDTH, + # 图像模式,默认为"RGBA" image_mode: str = "RGBA", + # 通道数,默认为4 n_channels: int = 4, + # 像素数组的类型,默认为np pixel_array_dtype: type = np.uint8, + # 光源位置,默认为[-10, 10, 10] light_source_position: Vect3 = np.array([-10, 10, 10]), - # Although vector graphics handle antialiasing fine - # without multisampling, for 3d scenes one might want - # to set samples to be greater than 0. + # 在处理矢量图形时,通常不需要使用多重采样来处理抗锯齿,因为矢量图形在处理抗锯齿时表现良好。 + # 但是,在处理3D场景时,可能需要将采样数设置为大于0的值以获得更好的图像质量 + # 采样数,默认为0 samples: int = 0, ): - self.background_image = background_image + # 代入各项参数 self.window = window + self.background_image = background_image self.default_pixel_shape = (pixel_width, pixel_height) self.fps = fps self.max_allowable_norm = max_allowable_norm @@ -56,21 +70,30 @@ def __init__( self.pixel_array_dtype = pixel_array_dtype self.light_source_position = light_source_position self.samples = samples - + # 像素值在RGB颜色空间中的最大值 self.rgb_max_val: float = np.iinfo(self.pixel_array_dtype).max + # 背景颜色的RGBA值 self.background_rgba: list[float] = list(color_to_rgba( background_color, background_opacity )) + # 初始化uniforms self.uniforms = dict() + # 初始化帧 self.init_frame(**frame_config) + # 初始化上下文 self.init_context() + # 初始化帧缓冲对象 self.init_fbo() + # 初始化光源 self.init_light_source() + # 初始化帧 def init_frame(self, **config) -> None: self.frame = CameraFrame(**config) + # 初始化上下文 def init_context(self) -> None: + # if self.window is None: self.ctx: moderngl.Context = moderngl.create_standalone_context() else: @@ -79,25 +102,31 @@ def init_context(self) -> None: self.ctx.enable(moderngl.PROGRAM_POINT_SIZE) self.ctx.enable(moderngl.BLEND) + # 初始化帧缓冲对象 def init_fbo(self) -> None: - # This is the buffer used when writing to a video/image file + # 创建一个用于写入视频/图像文件的帧缓冲对象 self.fbo_for_files = self.get_fbo(self.samples) - - # This is the frame buffer we'll draw into when emitting frames + # 创建一个用于绘制帧的帧缓冲对象 self.draw_fbo = self.get_fbo(samples=0) - + # 如果没有窗口对象 if self.window is None: + # 不设置窗口帧缓冲对象 self.window_fbo = None + # 后续绘制操作将会作用在这个帧缓冲对象上 self.fbo = self.fbo_for_files else: + # 检测当前OpenGL上下文中的帧缓冲对象,用于窗口显示 self.window_fbo = self.ctx.detect_framebuffer() + # 将self.fbo指向窗口帧缓冲对象,用于实时显示场景 self.fbo = self.window_fbo - + # 激活当前帧缓冲对象,使得后续的绘制操作都会作用在这个帧缓冲对象上 self.fbo.use() + # 初始化光源 def init_light_source(self) -> None: self.light_source = Point(self.light_source_position) + # 使用窗口的帧缓冲对象 def use_window_fbo(self, use: bool = True): assert(self.window is not None) if use: @@ -105,7 +134,9 @@ def use_window_fbo(self, use: bool = True): else: self.fbo = self.fbo_for_files - # Methods associated with the frame buffer + # 帧缓冲对象的有关方法 + + # 获取帧缓冲对象 def get_fbo( self, samples: int = 0 @@ -122,13 +153,13 @@ def get_fbo( ) ) + # 清空帧缓冲对象 def clear(self) -> None: self.fbo.clear(*self.background_rgba) + # 从一个帧缓冲对象复制到另一个帧缓冲对象 def blit(self, src_fbo, dst_fbo): - """ - Copy blocks between fbo's using Blit - """ + # 使用 Blit(位块传输)将一个帧缓冲对象(fbo)中的内容复制到另一个帧缓冲对象中 gl.glBindFramebuffer(gl.GL_READ_FRAMEBUFFER, src_fbo.glo) gl.glBindFramebuffer(gl.GL_DRAW_FRAMEBUFFER, dst_fbo.glo) gl.glBlitFramebuffer( @@ -137,79 +168,105 @@ def blit(self, src_fbo, dst_fbo): gl.GL_COLOR_BUFFER_BIT, gl.GL_LINEAR ) + # 获取帧缓冲对象的原始数据 def get_raw_fbo_data(self, dtype: str = 'f1') -> bytes: + # self.blit(self.fbo, self.draw_fbo) + # return self.draw_fbo.read( viewport=self.draw_fbo.viewport, components=self.n_channels, dtype=dtype, ) + # 获取图像 def get_image(self) -> Image.Image: return Image.frombytes( + # 图像模式 'RGBA', + # 图像的像素尺寸 self.get_pixel_shape(), + # 帧缓冲对象的原始数据 self.get_raw_fbo_data(), + # 数据来源、像素格式、扫描方向(0表示自上而下扫描)、行距(-1表示使用默认值) 'raw', 'RGBA', 0, -1 ) + # 获取像素数组 def get_pixel_array(self) -> np.ndarray: + # 获取帧缓冲对象的原始数据 raw = self.get_raw_fbo_data(dtype='f4') + # 将原始数据转换为NumPy数组 flat_arr = np.frombuffer(raw, dtype='f4') + # 将flat_arr按照帧缓冲对象的尺寸和通道数重新形状为三维数组arr arr = flat_arr.reshape([*reversed(self.draw_fbo.size), self.n_channels]) + # 将arr沿着垂直方向翻转,即将数组的行逆序排列 arr = arr[::-1] - # Convert from float + # 将数组中的值从浮点数转换为整数 return (self.rgb_max_val * arr).astype(self.pixel_array_dtype) - # Needed? + # 需要吗? + # 将帧缓冲对象的内容转换为纹理对象 def get_texture(self) -> moderngl.Texture: texture = self.ctx.texture( + # 获取帧缓冲对象的尺寸 size=self.fbo.size, + # 颜色组件数,默认为4,表示RGBA components=4, + # 纹理数据 data=self.get_raw_fbo_data(), + # 数据类型 dtype='f4' ) return texture - # Getting camera attributes + # 获取相机属性 + + # 获取像素大小 def get_pixel_size(self) -> float: return self.frame.get_width() / self.get_pixel_shape()[0] + # 获取像素形状 def get_pixel_shape(self) -> tuple[int, int]: return self.fbo.size + # 获取像素宽度 def get_pixel_width(self) -> int: return self.get_pixel_shape()[0] + # 获取像素高度 def get_pixel_height(self) -> int: return self.get_pixel_shape()[1] + # 获取宽高比 def get_aspect_ratio(self): pw, ph = self.get_pixel_shape() return pw / ph + # 获取帧的高度 def get_frame_height(self) -> float: return self.frame.get_height() + # 获取帧的宽度 def get_frame_width(self) -> float: return self.frame.get_width() + # 获取帧的形状 def get_frame_shape(self) -> tuple[float, float]: return (self.get_frame_width(), self.get_frame_height()) + # 获取帧中心 def get_frame_center(self) -> np.ndarray: return self.frame.get_center() + # 获取帧位置 def get_location(self) -> tuple[float, float, float]: return self.frame.get_implied_camera_location() + # 重置帧形状 def resize_frame_shape(self, fixed_dimension: bool = False) -> None: - """ - Changes frame_shape to match the aspect ratio - of the pixels, where fixed_dimension determines - whether frame_height or frame_width - remains fixed while the other changes accordingly. - """ + # 将帧的形状调整为与像素的宽高比相匹配 + # fixed_dimension参数确定了在另一个维度发生变化时,帧的高度或宽度是否保持固定 frame_height = self.get_frame_height() frame_width = self.get_frame_width() aspect_ratio = self.get_aspect_ratio() @@ -220,33 +277,61 @@ def resize_frame_shape(self, fixed_dimension: bool = False) -> None: self.frame.set_height(frame_height, stretch=true) self.frame.set_width(frame_width, stretch=true) - # Rendering + # 渲染 + + # 捕获指定的Mobject对象 def capture(self, *mobjects: Mobject) -> None: + # 清除当前帧缓冲对象的内容 self.clear() + # 刷新uniform变量 self.refresh_uniforms() + # 将当前帧缓冲对象设置为渲染目标 self.fbo.use() + # 对于传入的每个Mobject对象 for mobject in mobjects: + # 调用其render方法进行渲染,使用当前的渲染上下文和uniform变量 mobject.render(self.ctx, self.uniforms) + # 如果存在窗口并且帧缓冲对象不是窗口的帧缓冲对象 if self.window is not None and self.fbo is not self.window_fbo: + # 则将当前帧缓冲对象的内容复制到窗口的帧缓冲对象中,以便在窗口中显示 self.blit(self.fbo, self.window_fbo) + # 刷新uniform变量 def refresh_uniforms(self) -> None: + # 获取当前的帧对象 frame = self.frame + # 获取当前的帧对象的视图矩阵 view_matrix = frame.get_view_matrix() + # 获取当前的帧对象的光源位置 light_pos = self.light_source.get_location() + # 获取当前的帧对象的相机位置 cam_pos = self.frame.get_implied_camera_location() + # 更新uniforms属性 self.uniforms.update( + # 视图矩阵 view=tuple(view_matrix.T.flatten()), + # 焦距 focal_distance=frame.get_focal_distance() / frame.get_scale(), + # 帧形状 frame_scale=frame.get_scale(), + # 像素大小 pixel_size=self.get_pixel_size(), + # 相机位置 camera_position=tuple(cam_pos), + # 光源位置 light_position=tuple(light_pos), ) -# Mostly just defined so old scenes don't break +# 大部分只是定义,所以旧场景不会被破坏 +# 3D场景的相机对象 class ThreeDCamera(Camera): - def __init__(self, samples: int = 4, **kwargs): + # 3D场景相机的构造方法 + def __init__(self, + # 指定采样数为4 + samples: int = 4, + **kwargs + ): + # 只更改采样数,其他参数以父类为准 super().__init__(samples=samples, **kwargs) From 7ba41093d1cc53d51a317b10fedea9e7aa9d9703 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 17:42:58 +0800 Subject: [PATCH 15/26] Update camera_frame.py --- manimlib/camera/camera_frame.py | 72 ++++++++++++++++++++++++++++----- 1 file changed, 62 insertions(+), 10 deletions(-) diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py index e8077d7b3c..b29010d8c5 100644 --- a/manimlib/camera/camera_frame.py +++ b/manimlib/camera/camera_frame.py @@ -17,73 +17,86 @@ if TYPE_CHECKING: from manimlib.typing import Vect3 - +# 相机帧类 class CameraFrame(Mobject): + # 相机帧的初始化方法 def __init__( self, + # 相机帧的尺寸 frame_shape: tuple[float, float] = FRAME_SHAPE, + # 相机帧的中心点 center_point: Vect3 = ORIGIN, - # Field of view in the y direction + # y方向上的视场角 fovy: float = 45 * DEGREES, **kwargs, ): + # 父类的初始化方法 super().__init__(**kwargs) - + # 设置相机帧的默认旋转 self.uniforms["orientation"] = Rotation.identity().as_quat() + # 设置相机帧的默认视场 self.uniforms["fovy"] = fovy self.default_orientation = Rotation.identity() self.view_matrix = np.identity(4) self.camera_location = OUT # This will be updated by set_points - + # 设置相机帧的四个顶点 self.set_points(np.array([ORIGIN, LEFT, RIGHT, DOWN, UP])) self.set_width(frame_shape[0], stretch=True) self.set_height(frame_shape[1], stretch=True) self.move_to(center_point) + # 设置相机帧的旋转 def set_orientation(self, rotation: Rotation): self.uniforms["orientation"][:] = rotation.as_quat() return self + # 获取相机帧的旋转 def get_orientation(self): return Rotation.from_quat(self.uniforms["orientation"]) + # 设置相机帧的默认旋转 def make_orientation_default(self): self.default_orientation = self.get_orientation() return self + # 重置相机帧的默认状态 def to_default_state(self): self.set_shape(*FRAME_SHAPE) self.center() self.set_orientation(self.default_orientation) return self + # 获取相机帧的欧拉角 def get_euler_angles(self) -> np.ndarray: orientation = self.get_orientation() if all(orientation.as_quat() == [0, 0, 0, 1]): return np.zeros(3) return orientation.as_euler("zxz")[::-1] + # 获取相机帧的旋转角度(theta) def get_theta(self): return self.get_euler_angles()[0] + # 获取相机帧的旋转角度(phi) def get_phi(self): return self.get_euler_angles()[1] + # 获取相机帧的旋转角度(gamma) def get_gamma(self): return self.get_euler_angles()[2] + # 获取相机帧的缩放比例 def get_scale(self): return self.get_height() / FRAME_SHAPE[1] + # 获取相机帧的逆旋转矩阵 def get_inverse_camera_rotation_matrix(self): return self.get_orientation().as_matrix().T + # 获取相机帧的视图矩阵 def get_view_matrix(self, refresh=False): - """ - Returns a 4x4 for the affine transformation mapping a point - into the camera's internal coordinate system - """ + # 返回 4x4 的仿射变换映射点,进入相机的内部坐标系 if self._data_has_changed: shift = np.identity(4) rotation = np.identity(4) @@ -99,19 +112,25 @@ def get_view_matrix(self, refresh=False): return self.view_matrix + # 获取相机帧的逆视图矩阵 def get_inv_view_matrix(self): return np.linalg.inv(self.get_view_matrix()) + # 数据影响装饰器 @Mobject.affects_data + # 插值对象的状态 def interpolate(self, *args, **kwargs): super().interpolate(*args, **kwargs) + # 数据影响装饰器 @Mobject.affects_data + # 围绕给定的轴向旋转对象 def rotate(self, angle: float, axis: np.ndarray = OUT, **kwargs): rot = Rotation.from_rotvec(angle * normalize(axis)) self.set_orientation(rot * self.get_orientation()) return self + # 设置相机帧的欧拉角 def set_euler_angles( self, theta: float | None = None, @@ -130,6 +149,7 @@ def set_euler_angles( self.set_orientation(rot) return self + # def reorient( self, theta_degrees: float | None = None, @@ -143,62 +163,86 @@ def reorient( self.set_euler_angles(theta_degrees, phi_degrees, gamma_degrees, units=DEGREES) return self + # 设置相机帧的旋转角度(theta) def set_theta(self, theta: float): return self.set_euler_angles(theta=theta) + # 设置相机帧的旋转角度(phi) def set_phi(self, phi: float): return self.set_euler_angles(phi=phi) + # 设置相机帧的旋转角度(gamma) def set_gamma(self, gamma: float): return self.set_euler_angles(gamma=gamma) + # 获取相机帧的旋转角度(theta) def increment_theta(self, dtheta: float): self.rotate(dtheta, OUT) return self + # 获取相机帧的旋转角度(phi) def increment_phi(self, dphi: float): self.rotate(dphi, self.get_inverse_camera_rotation_matrix()[0]) return self + # 获取相机帧的旋转角度(gamma) def increment_gamma(self, dgamma: float): self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2]) return self + # 数据影响装饰器 @Mobject.affects_data + # 设置相机帧的焦距 def set_focal_distance(self, focal_distance: float): + # 根据给定的焦距计算视野角度,并更新uniforms字典中的"fovy"键值对应的值 self.uniforms["fovy"] = 2 * math.atan(0.5 * self.get_height() / focal_distance) return self + # 数据影响装饰器 @Mobject.affects_data + # 设置相机帧的视野 def set_field_of_view(self, field_of_view: float): + # 直接将给定的视野角度(field_of_view)设置为"fovy"对应的值,并更新uniforms字典中的"fovy"键值对应的值 self.uniforms["fovy"] = field_of_view return self + # 获取相机帧的形状(shape) def get_shape(self): return (self.get_width(), self.get_height()) + # 获取相机帧的宽高比(aspect ratio) def get_aspect_ratio(self): width, height = self.get_shape() return width / height + # 获取相机帧的中心(center) def get_center(self) -> np.ndarray: # Assumes first point is at the center return self.get_points()[0] + # 获取相机帧的宽度(width) def get_width(self) -> float: points = self.get_points() return points[2, 0] - points[1, 0] + # 获取相机帧的高度(height) def get_height(self) -> float: points = self.get_points() return points[4, 1] - points[3, 1] + # 获取相机帧的焦距(focal distance) def get_focal_distance(self) -> float: return 0.5 * self.get_height() / math.tan(0.5 * self.uniforms["fovy"]) + # 获取相机帧的视场角(field of view) def get_field_of_view(self) -> float: return self.uniforms["fovy"] - + + # 这段代码用于获取相机在场景中的位置。具体步骤如下: + # 检查数据是否已更改,如果是,则需要重新计算相机位置。 + # 获取相机的旋转矩阵中第三列,该列表示相机的朝向。 + # 获取相机到场景中心的距离(焦距)。 + # 计算相机的位置为场景中心加上相机朝向乘以焦距的结果。 def get_implied_camera_location(self) -> np.ndarray: if self._data_has_changed: to_camera = self.get_inverse_camera_rotation_matrix()[2] @@ -206,12 +250,20 @@ def get_implied_camera_location(self) -> np.ndarray: self.camera_location = self.get_center() + dist * to_camera return self.camera_location + # 将一个点(point)从相机视图坐标系转换到固定帧坐标系(fixed-frame coordinate system)中 def to_fixed_frame_point(self, point: Vect3, relative: bool = False): + # 获取相机的视图矩阵 view = self.get_view_matrix() + # 构造一个四维的点,如果relative为真,则最后一维设为0,否则设为1 point4d = [*point, 0 if relative else 1] + # 将四维点与视图矩阵的转置相乘,得到一个新的四维点,并取其前三个元素,即为转换后的点在固定帧坐标系中的位置 return np.dot(point4d, view.T)[:3] + # 将一个点从固定帧坐标系转换到相机视图坐标系中 def from_fixed_frame_point(self, point: Vect3, relative: bool = False): - inv_view = self.get_inv_view_matrix() + # 获取逆视图矩阵 + inv_view = self.get_inv_view_matrix() + # 构造一个四维的点,如果relative为真,则最后一维设为0,否则设为1 point4d = [*point, 0 if relative else 1] + # 将四维点与逆视图矩阵的转置相乘,得到一个新的四维点,并取其前三个元素,即为转换后的点在相机视图坐标系中的位置 return np.dot(point4d, inv_view.T)[:3] From ff76c7a33e9a12d3eb6d01ff9a8657c5c0a613fa Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 17:45:57 +0800 Subject: [PATCH 16/26] Update event_listner.py --- manimlib/event_handler/event_listner.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/manimlib/event_handler/event_listner.py b/manimlib/event_handler/event_listner.py index 5e742f3113..483d383063 100644 --- a/manimlib/event_handler/event_listner.py +++ b/manimlib/event_handler/event_listner.py @@ -4,28 +4,34 @@ if TYPE_CHECKING: from typing import Callable - from manimlib.event_handler.event_type import EventType from manimlib.mobject.mobject import Mobject - +# 事件监听器类 class EventListener(object): + # 初始化 def __init__( self, + # 监听的mobject mobject: Mobject, + # 监听的事件类型 event_type: EventType, + # 监听的回调函数 event_callback: Callable[[Mobject, dict[str]]] ): self.mobject = mobject self.event_type = event_type self.callback = event_callback + # 判断两个监听器是否相等 def __eq__(self, o: object) -> bool: return_val = False try: + # 比较的标准是监听的对象、事件类型和回调函数是否相等 return_val = self.callback == o.callback \ and self.mobject == o.mobject \ and self.event_type == o.event_type except: pass + # 只要有不同,都返回False return return_val From 996fe967a90599286a21b1edea4fbb6949b8a5d8 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 17:48:01 +0800 Subject: [PATCH 17/26] Update event_type.py --- manimlib/event_handler/event_type.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/manimlib/event_handler/event_type.py b/manimlib/event_handler/event_type.py index 6cd9f73e62..ac9f87fa1b 100644 --- a/manimlib/event_handler/event_type.py +++ b/manimlib/event_handler/event_type.py @@ -1,11 +1,18 @@ from enum import Enum - +# 事件类型的枚举类 class EventType(Enum): + # 鼠标移动事件 MouseMotionEvent = 'mouse_motion_event' + # 鼠标点击事件 MousePressEvent = 'mouse_press_event' + # 鼠标释放事件 MouseReleaseEvent = 'mouse_release_event' + # 鼠标拖拽事件 MouseDragEvent = 'mouse_drag_event' + # 鼠标滚动事件 MouseScrollEvent = 'mouse_scroll_event' + # 键盘按下事件 KeyPressEvent = 'key_press_event' + # 键盘释放事件 KeyReleaseEvent = 'key_release_event' From 6596f7693819c413fbbbef3d17db4ea2f4a2eb5a Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 18:25:09 +0800 Subject: [PATCH 18/26] Update geometry.py --- manimlib/mobject/geometry.py | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 2f1a4730fd..47f2c7d46b 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -204,48 +204,61 @@ def get_length(self) -> float: start, end = self.get_start_and_end() return get_norm(start - end) - +# 圆弧形状的图形类 class Arc(TipableVMobject): def __init__( self, + # 起始角度 start_angle: float = 0, + # 角度范围 angle: float = TAU / 4, + # 半径 radius: float = 1.0, + # 组件数量 n_components: int = 8, + # 圆弧中心点 arc_center: Vect3 = ORIGIN, **kwargs ): super().__init__(**kwargs) - + # 使用quadratic_bezier_points_for_arc函数生成表示弧形的点的列表,并将其设置为弧形的点集 + # 该函数生成一系列点,这些点可以用于近似表示一个弧 self.set_points(quadratic_bezier_points_for_arc(angle, n_components)) + # 围绕原点(ORIGIN)将弧形按给定的起始角度start_angle旋转。这可能用于使弧形从其初始角度开始 self.rotate(start_angle, about_point=ORIGIN) + # 以原点为中心,按给定的半径radius缩放弧形。这会调整弧形的大小,使其具有指定的半径 self.scale(radius, about_point=ORIGIN) + # 将弧形移动到指定的中心点arc_center。这会将弧形的位置移动到指定的中心位置,以便在画布上正确定位 self.shift(arc_center) + # 获取中心点 def get_arc_center(self) -> Vect3: - """ - Looks at the normals to the first two - anchors, and finds their intersection points - """ - # First two anchors and handles + # 首先获取圆弧的前两个锚点和它们对应的锚点 a1, h, a2 = self.get_points()[:3] - # Tangent vectors + # 计算出这两个点的切线向量 t1 = h - a1 t2 = h - a2 - # Normals + # 根据切线向量计算出法向量,即切线向量旋转90度 n1 = rotate_vector(t1, TAU / 4) n2 = rotate_vector(t2, TAU / 4) + # 求出法向量的交点即为圆弧的中心点 return find_intersection(a1, n1, a2, n2) + # 获取起始角度 def get_start_angle(self) -> float: + # 圆弧起点到圆心的向量计算出角度,并将结果归一化到0到2π之间 angle = angle_of_vector(self.get_start() - self.get_arc_center()) return angle % TAU + # 获取结束角度 def get_stop_angle(self) -> float: + # 圆弧终点到圆心的向量计算出角度,并将结果归一化到0到2π之间 angle = angle_of_vector(self.get_end() - self.get_arc_center()) return angle % TAU + # 移动圆弧中心点到指定位置 def move_arc_center_to(self, point: Vect3) -> Self: + # 通过平移操作,将圆弧的中心点移动到指定的位置 self.shift(point - self.get_arc_center()) return self From d6037eee64c42eb32f3a83a9b8f94e1a2c5f6bc5 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 18:28:32 +0800 Subject: [PATCH 19/26] Update geometry.py --- manimlib/mobject/geometry.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 47f2c7d46b..20ebd69fa4 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -45,7 +45,8 @@ DEFAULT_ARROW_TIP_WIDTH = 0.35 -# Deprecate? +# 弃用? +# 具有提示功能的图形类 class TipableVMobject(VMobject): """ Meant for shared functionality between Arc and Line. @@ -63,10 +64,29 @@ class TipableVMobject(VMobject): * Getters - Straightforward accessors, returning information pertaining to the TipableVMobject instance's tip(s), its length etc + + 用于圆弧和直线之间的共享功能。 + 功能可大致分为以下几组: + + * 添加、创建、修改提示 + - add_tip 在推送新提示之前调用 create_tip + 进入 TipableVMobject 的子对象列表 + - 风格和位置配置 + + * 检查提示 + - 布尔检查 TipableVM 对象是否有提示 + 和一个起始提示 + + *吸气剂 + - 简单的访问器,返回相关信息 + TipableVMobject 实例的提示、长度等 """ tip_config: dict = dict( + # 填充不透明度 fill_opacity=1.0, + # 描边宽度 stroke_width=0.0, + # 提示的样式,通常有三种选择:三角形、内部平滑和点 tip_style=0.0, # triangle=0, inner_smooth=1, dot=2 ) From 584c2297245c78811137807dd7ca10e1cb108fda Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 22:30:38 +0800 Subject: [PATCH 20/26] Update vectorized_mobject.py --- manimlib/mobject/types/vectorized_mobject.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a3bc87b99b..6df6c02fc1 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1488,22 +1488,36 @@ def __init__( self.match_style(vmobject, recurse=False) +# 高亮效果 class VHighlight(VGroup): + # 创建一个具有渐变描边的高亮效果 def __init__( self, + # 要高亮的对象 vmobject: VMobject, + # 高亮的层数,默认为5 n_layers: int = 5, + # 渐变的颜色范围 color_bounds: Tuple[ManimColor] = (GREY_C, GREY_E), + # 最大描边增加量,默认为5 max_stroke_addition: float = 5.0, ): + # 复制 vmobject 对象存储在 outline 变量中 outline = vmobject.replicate(n_layers) + # 设置 outline 中所有对象的填充不透明度为 0,即不显示填充 outline.set_fill(opacity=0) + # 生成一个数组,表示每一层的描边增加量,从 0 到 max_stroke_addition 均匀分布,长度为 n_layers + 1,然后去掉第一个元素(0) added_widths = np.linspace(0, max_stroke_addition, n_layers + 1)[1:] + # 根据 color_bounds 定义的颜色范围和 n_layers 层数,生成一个渐变的颜色列表 colors = color_gradient(color_bounds, n_layers) + # 使用 zip 函数将 outline、added_widths 和 colors 逐一组合,其中 reversed(outline) 是为了从外到内依次处理每一层 for part, added_width, color in zip(reversed(outline), added_widths, colors): + # 对每个 part 的家族成员(family member)进行迭代处理,确保对所有包含的子对象应用相同的描边和颜色设置 for sm in part.family_members_with_points(): + # 设置每个子对象的描边宽度为原始宽度加上对应层的增量,并设置颜色为对应的渐变色 sm.set_stroke( width=sm.get_stroke_width() + added_width, color=color, ) + # 调用父类 VGroup 的构造函数,创建了一个包含所有复制对象的组 super().__init__(*outline) From db93d5b562e3bc8b9df2d5450a4439d709ed6ac3 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sat, 17 Feb 2024 23:01:11 +0800 Subject: [PATCH 21/26] Update constants.py --- manimlib/constants.py | 74 +++++++++++++++++++++++++++++++------------ 1 file changed, 54 insertions(+), 20 deletions(-) diff --git a/manimlib/constants.py b/manimlib/constants.py index ac0345ec6d..1605f29967 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -7,92 +7,118 @@ from manimlib.typing import ManimColor, Vect3 -# Sizes relevant to default camera frame +# 与相机帧相关的默认属性 +# 屏幕的宽高比 ASPECT_RATIO: float = 16.0 / 9.0 +# 相机帧的宽度 FRAME_HEIGHT: float = 8.0 +# 相机帧的高度 FRAME_WIDTH: float = FRAME_HEIGHT * ASPECT_RATIO +# 相机帧的尺寸 FRAME_SHAPE: tuple[float, float] = (FRAME_WIDTH, FRAME_HEIGHT) +# 相机帧的中心 FRAME_Y_RADIUS: float = FRAME_HEIGHT / 2 FRAME_X_RADIUS: float = FRAME_WIDTH / 2 - +# 默认的像素宽高 DEFAULT_PIXEL_HEIGHT: int = 1080 DEFAULT_PIXEL_WIDTH: int = 1920 +# 默认的帧率 DEFAULT_FPS: int = 30 - +# 最小的缩放系数 SMALL_BUFF: float = 0.1 +# 中小的缩放系数 MED_SMALL_BUFF: float = 0.25 +# 中大的缩放系数 MED_LARGE_BUFF: float = 0.5 +# 最大的缩放系数 LARGE_BUFF: float = 1 - +# 默认的mobject与屏幕边缘的距离 DEFAULT_MOBJECT_TO_EDGE_BUFFER: float = MED_LARGE_BUFF +# 默认的mobject与mobject之间的距离 DEFAULT_MOBJECT_TO_MOBJECT_BUFFER: float = MED_SMALL_BUFF - -# In seconds +# 默认等待时间,以秒为单位 DEFAULT_WAIT_TIME: float = 1.0 - +# 三维空间中的原点位置 ORIGIN: Vect3 = np.array([0., 0., 0.]) + +# 分别表示三维空间中的上、下、右、左、内、外方向的单位向量 UP: Vect3 = np.array([0., 1., 0.]) DOWN: Vect3 = np.array([0., -1., 0.]) RIGHT: Vect3 = np.array([1., 0., 0.]) LEFT: Vect3 = np.array([-1., 0., 0.]) IN: Vect3 = np.array([0., 0., -1.]) OUT: Vect3 = np.array([0., 0., 1.]) + +# 分别表示三维空间中的X轴、Y轴、Z轴的单位向量 X_AXIS: Vect3 = np.array([1., 0., 0.]) Y_AXIS: Vect3 = np.array([0., 1., 0.]) Z_AXIS: Vect3 = np.array([0., 0., 1.]) +# 表示一个空的点集合 NULL_POINTS = np.array([[0., 0., 0.]]) -# Useful abbreviations for diagonals +# 对角线方向的单位向量 UL: Vect3 = UP + LEFT UR: Vect3 = UP + RIGHT DL: Vect3 = DOWN + LEFT DR: Vect3 = DOWN + RIGHT +# 相对于窗口中心的顶部、底部、左侧、右侧的位置向量 TOP: Vect3 = FRAME_Y_RADIUS * UP BOTTOM: Vect3 = FRAME_Y_RADIUS * DOWN LEFT_SIDE: Vect3 = FRAME_X_RADIUS * LEFT RIGHT_SIDE: Vect3 = FRAME_X_RADIUS * RIGHT +# 分别表示圆周率π、圆周率的两倍τ、一个角度的弧度值、一个弧度的角度值 PI: float = np.pi -TAU: float = 2 * PI -DEGREES: float = TAU / 360 -# Nice to have a constant for readability -# when juxtaposed with expressions like 30 * DEGREES -RADIANS: float = 1 +TAU: float = 2 * PI # ≈ 6.2831 +DEGREES: float = TAU / 360 # ≈ 0.0175 表示1°的角对应约0.0175的弧度值 +# 当与 30 * DEGREES 等表达式并置时,有一个常量以提高可读性是件好事 +RADIANS: float = 1 # 1的弧度对应约57.2958的角度值 +# ffmpeg可执行文件的路径 FFMPEG_BIN: str = "ffmpeg" +# 将不同类型的线段连接方式与数字进行映射,这些数字用于表示线段的连接方式 JOINT_TYPE_MAP: dict = { + # 无连接,线段之间没有特殊的连接方式 "no_joint": 0, + # 自动连接,线段之间以自动连接的方式连接 "auto": 1, + # 斜角连接,线段之间以斜角相连接 "bevel": 2, + # 斜接连接,当线段之间的角度很小或很尖时,会以尖角相接 "miter": 3, } -# Related to Text +# 关于文本 + +# 用于指定文本的样式,例如正常、斜体、倾斜或粗体 NORMAL: str = "NORMAL" ITALIC: str = "ITALIC" OBLIQUE: str = "OBLIQUE" BOLD: str = "BOLD" +# 默认的描边宽度,用于指定文本等对象的描边宽度 DEFAULT_STROKE_WIDTH: float = 4 -# For keyboard interactions +# 用于键盘交互的特殊键的Unicode符号码,包括Ctrl、Shift、Command、Delete和箭头键等 CTRL_SYMBOL: int = 65508 SHIFT_SYMBOL: int = 65505 COMMAND_SYMBOL: int = 65517 DELETE_SYMBOL: int = 65288 ARROW_SYMBOLS: list[int] = list(range(65361, 65365)) +# 用于键盘交互的修饰键,分别表示Shift、Ctrl和Command修饰键的值 SHIFT_MODIFIER: int = 1 CTRL_MODIFIER: int = 2 COMMAND_MODIFIER: int = 64 -# Colors +# 关于颜色 +# 颜色大全 BLUE_E: ManimColor = "#1C758A" BLUE_D: ManimColor = "#29ABCA" BLUE_C: ManimColor = "#58C4DD" @@ -148,8 +174,16 @@ GREEN_SCREEN: ManimColor = "#00FF00" ORANGE: ManimColor = "#FF862F" +# 预定义的一组颜色值 MANIM_COLORS: List[ManimColor] = [ - BLACK, GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, WHITE, + BLACK, + WHITE, + GREY_BROWN, + DARK_BROWN, + LIGHT_BROWN, + PINK, + LIGHT_PINK, + GREY_E, GREY_D, GREY_C, GREY_B, GREY_A, BLUE_E, BLUE_D, BLUE_C, BLUE_B, BLUE_A, TEAL_E, TEAL_D, TEAL_C, TEAL_B, TEAL_A, GREEN_E, GREEN_D, GREEN_C, GREEN_B, GREEN_A, @@ -158,11 +192,9 @@ RED_E, RED_D, RED_C, RED_B, RED_A, MAROON_E, MAROON_D, MAROON_C, MAROON_B, MAROON_A, PURPLE_E, PURPLE_D, PURPLE_C, PURPLE_B, PURPLE_A, - GREY_BROWN, DARK_BROWN, LIGHT_BROWN, - PINK, LIGHT_PINK, ] -# Abbreviated names for the "median" colors +# 采用中间色调作为简写的颜色名称 BLUE: ManimColor = BLUE_C TEAL: ManimColor = TEAL_C GREEN: ManimColor = GREEN_C @@ -173,4 +205,6 @@ PURPLE: ManimColor = PURPLE_C GREY: ManimColor = GREY_C +# 经常使用的颜色,包括淡蓝色、绿色、黄色和红色 COLORMAP_3B1B: List[ManimColor] = [BLUE_E, GREEN, YELLOW, RED] + From d14b1e6f35236a473becac42b250ad2c5f1428ee Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:04:48 +0800 Subject: [PATCH 22/26] Update scene.py --- manimlib/scene/scene.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index af31dd42e7..a8f56dfd38 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -60,7 +60,28 @@ # 退出交互模式 QUIT_KEY = 'q' - +# (修改文件后)重新运行场景的处理器类 +class RerunSceneHandler(FileSystemEventHandler): + # + def __init__(self, queue): + super().__init__() + self.queue = queue + # 当文件或目录被修改时调用 + def on_modified(self, event): + # 将重新运行场景的指令放入队列中 + self.queue.put(("rerun_file", [], {})) + # 事件包括DirModifiedEvent(目录修改事件)或FileModifiedEvent(文件修改事件) + + +# Scene是动画的画布,它提供了管理图形对象和动画的工具。通常,一个manim脚本包含一个继承自Scene的类,用户会在这个类中重写Scene.construct方法。 +# 通过调用Scene.add可以将图形对象显示在屏幕上,通过调用Scene.remove可以将图形对象从屏幕上移除。所有当前在屏幕上的图形对象都存储在Scene.mobjects属性中。通过调用Scene.play可以播放动画。 +# Scene内部通过调用Scene.render来进行渲染。这个过程中会依次调用Scene.setup、Scene.construct和Scene.tear_down。 +# 不建议在用户自定义的Scene中重写__init__方法。如果需要在Scene渲染之前运行一些代码,可以使用Scene.setup方法。 +# 示例代码: +# class MyScene(Scene): +# def construct(self): +# self.play(Write(Text("Hello World!"))) +# 场景类 class Scene(object): # 随机种子,初始为0 random_seed: int = 0 From 448e86f2acf01be8b98946ed4edf272058265329 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sun, 18 Feb 2024 12:26:04 +0800 Subject: [PATCH 23/26] Update scene.py --- manimlib/scene/scene.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a8f56dfd38..75736ba5a3 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -740,16 +740,22 @@ def post_play(self): self.num_plays += 1 + def begin_animations(self, animations: Iterable[Animation]) -> None: for animation in animations: animation.begin() - # Anything animated that's not already in the - # scene gets added to the scene. Note, for - # animated mobjects that are in the family of - # those on screen, this can result in a restructuring - # of the scene.mobjects list, which is usually desired. + # 场景中尚未存在的任何动画都会添加到场景中。 + # 请注意,对于屏幕上的动画 mobject 系列,这可能会导致 scene.mobjects 列表的重组,这通常是需要的。 if animation.mobject not in self.mobjects: self.add(animation.mobject) + # 在场景中开始动画的方法。它遍历场景中的所有动画对象,并逐个开始它们的动画。 + # 如果使用的渲染器是CAIRO,它会将所有不移动的对象绘制到屏幕上,因此他们不必每一帧都重新渲染。 + def begin_animations(self) -> None: + for animation in self.animations: + animation._setup_scene(self) + animation.begin() + if config.renderer == RendererType.CAIRO: + (self.moving_mobjects, self.static_mobjects,) = self.get_moving_and_static_mobjects(self.animations) def progress_through_animations(self, animations: Iterable[Animation]) -> None: last_t = 0 From 1cac42a20cc1a414052b2157bbff9c3c0b478eb6 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Sun, 18 Feb 2024 20:52:00 +0800 Subject: [PATCH 24/26] Create *scene.py --- manimlib/scene/*scene.py | 1788 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 1788 insertions(+) create mode 100644 manimlib/scene/*scene.py diff --git a/manimlib/scene/*scene.py b/manimlib/scene/*scene.py new file mode 100644 index 0000000000..44e8bc8206 --- /dev/null +++ b/manimlib/scene/*scene.py @@ -0,0 +1,1788 @@ +"""Basic canvas for animations.""" + +from __future__ import annotations + +from manim.utils.parameter_parsing import flatten_iterable_parameters + +__all__ = ["Scene"] + +import copy +import datetime +import inspect +import platform +import random +import threading +import time +import types +from queue import Queue + +import srt + +from manim.scene.section import DefaultSectionType + +try: + import dearpygui.dearpygui as dpg + + dearpygui_imported = True +except ImportError: + dearpygui_imported = False +from typing import TYPE_CHECKING + +import numpy as np +from tqdm import tqdm +from watchdog.events import FileSystemEventHandler +from watchdog.observers import Observer + +from manim.mobject.mobject import Mobject +from manim.mobject.opengl.opengl_mobject import OpenGLPoint + +from .. import config, logger +from ..animation.animation import Animation, Wait, prepare_animation +from ..camera.camera import Camera +from ..constants import * +from ..gui.gui import configure_pygui +from ..renderer.cairo_renderer import CairoRenderer +from ..renderer.opengl_renderer import OpenGLRenderer +from ..renderer.shader import Object3D +from ..utils import opengl, space_ops +from ..utils.exceptions import EndSceneEarlyException, RerunSceneException +from ..utils.family import extract_mobject_family_members +from ..utils.family_ops import restructure_list_to_exclude_certain_family_members +from ..utils.file_ops import open_media_file +from ..utils.iterables import list_difference_update, list_update + +if TYPE_CHECKING: + from typing import Callable, Iterable + + +# (修改文件后)重新运行场景的处理器类 +class RerunSceneHandler(FileSystemEventHandler): + def __init__(self, queue): + super().__init__() + self.queue = queue + # 当文件或目录被修改时调用 + def on_modified(self, event): + # 将重新运行场景的指令放入队列中 + self.queue.put(("rerun_file", [], {})) + # 这里的事件包括DirModifiedEvent(目录修改事件)或FileModifiedEvent(文件修改事件) + +# Scene是动画的画布,它提供了管理图形对象和动画的工具 +# 通常,一个manim脚本包含一个继承自Scene的类 +# 用户会在这个类中重写Scene.construct方法 +# 通过调用Scene.add可以将图形对象显示在屏幕上 +# 通过调用Scene.remove可以将图形对象从屏幕上移除 +# 所有当前在屏幕上的图形对象都存储在Scene.mobjects属性中 +# 通过调用Scene.play可以播放动画 +# Scene内部通过调用Scene.render来进行渲染。这个过程中会依次调用Scene.setup、Scene.construct和Scene.tear_down +# 不建议在用户自定义的Scene中重写__init__方法。如果需要在Scene渲染之前运行一些代码,可以使用Scene.setup方法 +# 示例代码: +# class MyScene(Scene): +# def construct(self): +# self.play(Write(Text("Hello World!"))) +class Scene: + def __init__( + self, + # 渲染器 + renderer=None, + # 相机 + camera_class=Camera, + # 是否总是更新mobjects,默认为False + always_update_mobjects=False, + # 随机数种子 + random_seed=None, + # 是否跳过动画,默认为False + skip_animations=False, + ): + self.camera_class = camera_class + self.always_update_mobjects = always_update_mobjects + self.random_seed = random_seed + self.skip_animations = skip_animations + # 动画 + self.animations = None + # 停止条件 + self.stop_condition = None + # 动态mobject列表 + self.moving_mobjects = [] + # 静态mobject列表 + self.static_mobjects = [] + # 时间进度 + self.time_progression = None + # 动画持续时间 + self.duration = None + # 上次的时间进度 + self.last_t = None + # 重新运行场景的队列 + self.queue = Queue() + # 是否跳过动画预览,默认为False + self.skip_animation_preview = False + # 网格 + self.meshes = [] + # 相机目标位置 + self.camera_target = ORIGIN + # 控件列表 + self.widgets = [] + # 是否导入dearpygui + self.dearpygui_imported = dearpygui_imported + # 更新器列表 + self.updaters = [] + # 点光源列表 + self.point_lights = [] + # 环境光 + self.ambient_light = None + # 不同的键映射到不同的函数上的字典 + self.key_to_function_map = {} + # 鼠标按下回调列表 + self.mouse_press_callbacks = [] + # 交互模式 + self.interactive_mode = False + # 检查配置中的渲染器是否为OpenGL类型 + if config.renderer == RendererType.OPENGL: + # 交互相关的物件,如果是,它将创建两个点mouse_point和mouse_drag_point + self.mouse_point = OpenGLPoint() + self.mouse_drag_point = OpenGLPoint() + # 如果未提供渲染器,则会创建一个OpenGL渲染器 + if renderer is None: + renderer = OpenGLRenderer() + # 检查是否已经提供了渲染器。如果未提供,则创建一个CairoRenderer实例,并根据情况传递相机类和跳过动画的设置 + if renderer is None: + # 然后,它将初始化创建的渲染器,并将其存储在场景的renderer属性中 + self.renderer = CairoRenderer( + camera_class=self.camera_class, + skip_animations=self.skip_animations, + ) + # 如果已经提供了渲染器,则直接使用提供的渲染器,并初始化它 + else: + self.renderer = renderer + self.renderer.init_scene(self) + + self.mobjects = [] + # TODO, remove need for foreground mobjects + self.foreground_mobjects = [] + if self.random_seed is not None: + random.seed(self.random_seed) + np.random.seed(self.random_seed) + + @property + def camera(self): + return self.renderer.camera + + def __deepcopy__(self, clone_from_id): + cls = self.__class__ + result = cls.__new__(cls) + clone_from_id[id(self)] = result + for k, v in self.__dict__.items(): + if k in ["renderer", "time_progression"]: + continue + if k == "camera_class": + setattr(result, k, v) + setattr(result, k, copy.deepcopy(v, clone_from_id)) + result.mobject_updater_lists = [] + + # Update updaters + for mobject in self.mobjects: + cloned_updaters = [] + for updater in mobject.updaters: + # Make the cloned updater use the cloned Mobjects as free variables + # rather than the original ones. Analyzing function bytecode with the + # dis module will help in understanding this. + # https://docs.python.org/3/library/dis.html + # TODO: Do the same for function calls recursively. + free_variable_map = inspect.getclosurevars(updater).nonlocals + cloned_co_freevars = [] + cloned_closure = [] + for free_variable_name in updater.__code__.co_freevars: + free_variable_value = free_variable_map[free_variable_name] + + # If the referenced variable has not been cloned, raise. + if id(free_variable_value) not in clone_from_id: + raise Exception( + f"{free_variable_name} is referenced from an updater " + "but is not an attribute of the Scene, which isn't " + "allowed.", + ) + + # Add the cloned object's name to the free variable list. + cloned_co_freevars.append(free_variable_name) + + # Add a cell containing the cloned object's reference to the + # closure list. + cloned_closure.append( + types.CellType(clone_from_id[id(free_variable_value)]), + ) + + cloned_updater = types.FunctionType( + updater.__code__.replace(co_freevars=tuple(cloned_co_freevars)), + updater.__globals__, + updater.__name__, + updater.__defaults__, + tuple(cloned_closure), + ) + cloned_updaters.append(cloned_updater) + mobject_clone = clone_from_id[id(mobject)] + mobject_clone.updaters = cloned_updaters + if len(cloned_updaters) > 0: + result.mobject_updater_lists.append((mobject_clone, cloned_updaters)) + return result + + def render(self, preview: bool = False): + """ + Renders this Scene. + + Parameters + --------- + preview + If true, opens scene in a file viewer. + """ + self.setup() + try: + self.construct() + except EndSceneEarlyException: + pass + except RerunSceneException as e: + self.remove(*self.mobjects) + self.renderer.clear_screen() + self.renderer.num_plays = 0 + return True + self.tear_down() + # We have to reset these settings in case of multiple renders. + self.renderer.scene_finished(self) + + # Show info only if animations are rendered or to get image + if ( + self.renderer.num_plays + or config["format"] == "png" + or config["save_last_frame"] + ): + logger.info( + f"Rendered {str(self)}\nPlayed {self.renderer.num_plays} animations", + ) + + # If preview open up the render after rendering. + if preview: + config["preview"] = True + + if config["preview"] or config["show_in_file_browser"]: + open_media_file(self.renderer.file_writer) + + def setup(self): + """ + This is meant to be implemented by any scenes which + are commonly subclassed, and have some common setup + involved before the construct method is called. + """ + pass + + def tear_down(self): + """ + This is meant to be implemented by any scenes which + are commonly subclassed, and have some common method + to be invoked before the scene ends. + """ + pass + + def construct(self): + """Add content to the Scene. + + From within :meth:`Scene.construct`, display mobjects on screen by calling + :meth:`Scene.add` and remove them from screen by calling :meth:`Scene.remove`. + All mobjects currently on screen are kept in :attr:`Scene.mobjects`. Play + animations by calling :meth:`Scene.play`. + + Notes + ----- + Initialization code should go in :meth:`Scene.setup`. Termination code should + go in :meth:`Scene.tear_down`. + + Examples + -------- + A typical manim script includes a class derived from :class:`Scene` with an + overridden :meth:`Scene.contruct` method: + + .. code-block:: python + + class MyScene(Scene): + def construct(self): + self.play(Write(Text("Hello World!"))) + + See Also + -------- + :meth:`Scene.setup` + :meth:`Scene.render` + :meth:`Scene.tear_down` + + """ + pass # To be implemented in subclasses + + def next_section( + self, + name: str = "unnamed", + type: str = DefaultSectionType.NORMAL, + skip_animations: bool = False, + ) -> None: + """Create separation here; the last section gets finished and a new one gets created. + ``skip_animations`` skips the rendering of all animations in this section. + Refer to :doc:`the documentation` on how to use sections. + """ + self.renderer.file_writer.next_section(name, type, skip_animations) + + def __str__(self): + return self.__class__.__name__ + + def get_attrs(self, *keys: str): + """ + Gets attributes of a scene given the attribute's identifier/name. + + Parameters + ---------- + *keys + Name(s) of the argument(s) to return the attribute of. + + Returns + ------- + list + List of attributes of the passed identifiers. + """ + return [getattr(self, key) for key in keys] + + def update_mobjects(self, dt: float): + """ + Begins updating all mobjects in the Scene. + + Parameters + ---------- + dt + Change in time between updates. Defaults (mostly) to 1/frames_per_second + """ + for mobject in self.mobjects: + mobject.update(dt) + + def update_meshes(self, dt): + for obj in self.meshes: + for mesh in obj.get_family(): + mesh.update(dt) + + def update_self(self, dt: float): + """Run all scene updater functions. + + Among all types of update functions (mobject updaters, mesh updaters, + scene updaters), scene update functions are called last. + + Parameters + ---------- + dt + Scene time since last update. + + See Also + -------- + :meth:`.Scene.add_updater` + :meth:`.Scene.remove_updater` + """ + for func in self.updaters: + func(dt) + + def should_update_mobjects(self) -> bool: + """ + Returns True if the mobjects of this scene should be updated. + + In particular, this checks whether + + - the :attr:`always_update_mobjects` attribute of :class:`.Scene` + is set to ``True``, + - the :class:`.Scene` itself has time-based updaters attached, + - any mobject in this :class:`.Scene` has time-based updaters attached. + + This is only called when a single Wait animation is played. + """ + wait_animation = self.animations[0] + if wait_animation.is_static_wait is None: + should_update = ( + self.always_update_mobjects + or self.updaters + or wait_animation.stop_condition is not None + or any( + mob.has_time_based_updater() + for mob in self.get_mobject_family_members() + ) + ) + wait_animation.is_static_wait = not should_update + return not wait_animation.is_static_wait + + def get_top_level_mobjects(self): + """ + Returns all mobjects which are not submobjects. + + Returns + ------- + list + List of top level mobjects. + """ + # Return only those which are not in the family + # of another mobject from the scene + families = [m.get_family() for m in self.mobjects] + + def is_top_level(mobject): + num_families = sum((mobject in family) for family in families) + return num_families == 1 + + return list(filter(is_top_level, self.mobjects)) + + def get_mobject_family_members(self): + """ + Returns list of family-members of all mobjects in scene. + If a Circle() and a VGroup(Rectangle(),Triangle()) were added, + it returns not only the Circle(), Rectangle() and Triangle(), but + also the VGroup() object. + + Returns + ------- + list + List of mobject family members. + """ + if config.renderer == RendererType.OPENGL: + family_members = [] + for mob in self.mobjects: + family_members.extend(mob.get_family()) + return family_members + elif config.renderer == RendererType.CAIRO: + return extract_mobject_family_members( + self.mobjects, + use_z_index=self.renderer.camera.use_z_index, + ) + + def add(self, *mobjects: Mobject): + """ + Mobjects will be displayed, from background to + foreground in the order with which they are added. + + Parameters + --------- + *mobjects + Mobjects to add. + + Returns + ------- + Scene + The same scene after adding the Mobjects in. + + """ + if config.renderer == RendererType.OPENGL: + new_mobjects = [] + new_meshes = [] + for mobject_or_mesh in mobjects: + if isinstance(mobject_or_mesh, Object3D): + new_meshes.append(mobject_or_mesh) + else: + new_mobjects.append(mobject_or_mesh) + self.remove(*new_mobjects) + self.mobjects += new_mobjects + self.remove(*new_meshes) + self.meshes += new_meshes + elif config.renderer == RendererType.CAIRO: + mobjects = [*mobjects, *self.foreground_mobjects] + self.restructure_mobjects(to_remove=mobjects) + self.mobjects += mobjects + if self.moving_mobjects: + self.restructure_mobjects( + to_remove=mobjects, + mobject_list_name="moving_mobjects", + ) + self.moving_mobjects += mobjects + return self + + def add_mobjects_from_animations(self, animations): + curr_mobjects = self.get_mobject_family_members() + for animation in animations: + if animation.is_introducer(): + continue + # Anything animated that's not already in the + # scene gets added to the scene + mob = animation.mobject + if mob is not None and mob not in curr_mobjects: + self.add(mob) + curr_mobjects += mob.get_family() + + def remove(self, *mobjects: Mobject): + """ + Removes mobjects in the passed list of mobjects + from the scene and the foreground, by removing them + from "mobjects" and "foreground_mobjects" + + Parameters + ---------- + *mobjects + The mobjects to remove. + """ + if config.renderer == RendererType.OPENGL: + mobjects_to_remove = [] + meshes_to_remove = set() + for mobject_or_mesh in mobjects: + if isinstance(mobject_or_mesh, Object3D): + meshes_to_remove.add(mobject_or_mesh) + else: + mobjects_to_remove.append(mobject_or_mesh) + self.mobjects = restructure_list_to_exclude_certain_family_members( + self.mobjects, + mobjects_to_remove, + ) + self.meshes = list( + filter(lambda mesh: mesh not in set(meshes_to_remove), self.meshes), + ) + return self + elif config.renderer == RendererType.CAIRO: + for list_name in "mobjects", "foreground_mobjects": + self.restructure_mobjects(mobjects, list_name, False) + return self + + def replace(self, old_mobject: Mobject, new_mobject: Mobject) -> None: + """Replace one mobject in the scene with another, preserving draw order. + + If ``old_mobject`` is a submobject of some other Mobject (e.g. a + :class:`.Group`), the new_mobject will replace it inside the group, + without otherwise changing the parent mobject. + + Parameters + ---------- + old_mobject + The mobject to be replaced. Must be present in the scene. + new_mobject + A mobject which must not already be in the scene. + + """ + if old_mobject is None or new_mobject is None: + raise ValueError("Specified mobjects cannot be None") + + def replace_in_list( + mobj_list: list[Mobject], old_m: Mobject, new_m: Mobject + ) -> bool: + # We use breadth-first search because some Mobjects get very deep and + # we expect top-level elements to be the most common targets for replace. + for i in range(0, len(mobj_list)): + # Is this the old mobject? + if mobj_list[i] == old_m: + # If so, write the new object to the same spot and stop looking. + mobj_list[i] = new_m + return True + # Now check all the children of all these mobs. + for mob in mobj_list: # noqa: SIM110 + if replace_in_list(mob.submobjects, old_m, new_m): + # If we found it in a submobject, stop looking. + return True + # If we did not find the mobject in the mobject list or any submobjects, + # (or the list was empty), indicate we did not make the replacement. + return False + + # Make use of short-circuiting conditionals to check mobjects and then + # foreground_mobjects + replaced = replace_in_list( + self.mobjects, old_mobject, new_mobject + ) or replace_in_list(self.foreground_mobjects, old_mobject, new_mobject) + + if not replaced: + raise ValueError(f"Could not find {old_mobject} in scene") + + def add_updater(self, func: Callable[[float], None]) -> None: + """Add an update function to the scene. + + The scene updater functions are run every frame, + and they are the last type of updaters to run. + + .. WARNING:: + + When using the Cairo renderer, scene updaters that + modify mobjects are not detected in the same way + that mobject updaters are. To be more concrete, + a mobject only modified via a scene updater will + not necessarily be added to the list of *moving + mobjects* and thus might not be updated every frame. + + TL;DR: Use mobject updaters to update mobjects. + + Parameters + ---------- + func + The updater function. It takes a float, which is the + time difference since the last update (usually equal + to the frame rate). + + See also + -------- + :meth:`.Scene.remove_updater` + :meth:`.Scene.update_self` + """ + self.updaters.append(func) + + def remove_updater(self, func: Callable[[float], None]) -> None: + """Remove an update function from the scene. + + Parameters + ---------- + func + The updater function to be removed. + + See also + -------- + :meth:`.Scene.add_updater` + :meth:`.Scene.update_self` + """ + self.updaters = [f for f in self.updaters if f is not func] + + def restructure_mobjects( + self, + to_remove: Mobject, + mobject_list_name: str = "mobjects", + extract_families: bool = True, + ): + """ + tl:wr + If your scene has a Group(), and you removed a mobject from the Group, + this dissolves the group and puts the rest of the mobjects directly + in self.mobjects or self.foreground_mobjects. + + In cases where the scene contains a group, e.g. Group(m1, m2, m3), but one + of its submobjects is removed, e.g. scene.remove(m1), the list of mobjects + will be edited to contain other submobjects, but not m1, e.g. it will now + insert m2 and m3 to where the group once was. + + Parameters + ---------- + to_remove + The Mobject to remove. + + mobject_list_name + The list of mobjects ("mobjects", "foreground_mobjects" etc) to remove from. + + extract_families + Whether the mobject's families should be recursively extracted. + + Returns + ------- + Scene + The Scene mobject with restructured Mobjects. + """ + if extract_families: + to_remove = extract_mobject_family_members( + to_remove, + use_z_index=self.renderer.camera.use_z_index, + ) + _list = getattr(self, mobject_list_name) + new_list = self.get_restructured_mobject_list(_list, to_remove) + setattr(self, mobject_list_name, new_list) + return self + + def get_restructured_mobject_list(self, mobjects: list, to_remove: list): + """ + Given a list of mobjects and a list of mobjects to be removed, this + filters out the removable mobjects from the list of mobjects. + + Parameters + ---------- + + mobjects + The Mobjects to check. + + to_remove + The list of mobjects to remove. + + Returns + ------- + list + The list of mobjects with the mobjects to remove removed. + """ + + new_mobjects = [] + + def add_safe_mobjects_from_list(list_to_examine, set_to_remove): + for mob in list_to_examine: + if mob in set_to_remove: + continue + intersect = set_to_remove.intersection(mob.get_family()) + if intersect: + add_safe_mobjects_from_list(mob.submobjects, intersect) + else: + new_mobjects.append(mob) + + add_safe_mobjects_from_list(mobjects, set(to_remove)) + return new_mobjects + + # TODO, remove this, and calls to this + def add_foreground_mobjects(self, *mobjects: Mobject): + """ + Adds mobjects to the foreground, and internally to the list + foreground_mobjects, and mobjects. + + Parameters + ---------- + *mobjects + The Mobjects to add to the foreground. + + Returns + ------ + Scene + The Scene, with the foreground mobjects added. + """ + self.foreground_mobjects = list_update(self.foreground_mobjects, mobjects) + self.add(*mobjects) + return self + + def add_foreground_mobject(self, mobject: Mobject): + """ + Adds a single mobject to the foreground, and internally to the list + foreground_mobjects, and mobjects. + + Parameters + ---------- + mobject + The Mobject to add to the foreground. + + Returns + ------ + Scene + The Scene, with the foreground mobject added. + """ + return self.add_foreground_mobjects(mobject) + + def remove_foreground_mobjects(self, *to_remove: Mobject): + """ + Removes mobjects from the foreground, and internally from the list + foreground_mobjects. + + Parameters + ---------- + *to_remove + The mobject(s) to remove from the foreground. + + Returns + ------ + Scene + The Scene, with the foreground mobjects removed. + """ + self.restructure_mobjects(to_remove, "foreground_mobjects") + return self + + def remove_foreground_mobject(self, mobject: Mobject): + """ + Removes a single mobject from the foreground, and internally from the list + foreground_mobjects. + + Parameters + ---------- + mobject + The mobject to remove from the foreground. + + Returns + ------ + Scene + The Scene, with the foreground mobject removed. + """ + return self.remove_foreground_mobjects(mobject) + + def bring_to_front(self, *mobjects: Mobject): + """ + Adds the passed mobjects to the scene again, + pushing them to he front of the scene. + + Parameters + ---------- + *mobjects + The mobject(s) to bring to the front of the scene. + + Returns + ------ + Scene + The Scene, with the mobjects brought to the front + of the scene. + """ + self.add(*mobjects) + return self + + def bring_to_back(self, *mobjects: Mobject): + """ + Removes the mobject from the scene and + adds them to the back of the scene. + + Parameters + ---------- + *mobjects + The mobject(s) to push to the back of the scene. + + Returns + ------ + Scene + The Scene, with the mobjects pushed to the back + of the scene. + """ + self.remove(*mobjects) + self.mobjects = list(mobjects) + self.mobjects + return self + + def clear(self): + """ + Removes all mobjects present in self.mobjects + and self.foreground_mobjects from the scene. + + Returns + ------ + Scene + The Scene, with all of its mobjects in + self.mobjects and self.foreground_mobjects + removed. + """ + self.mobjects = [] + self.foreground_mobjects = [] + return self + + def get_moving_mobjects(self, *animations: Animation): + """ + Gets all moving mobjects in the passed animation(s). + + Parameters + ---------- + *animations + The animations to check for moving mobjects. + + Returns + ------ + list + The list of mobjects that could be moving in + the Animation(s) + """ + # Go through mobjects from start to end, and + # as soon as there's one that needs updating of + # some kind per frame, return the list from that + # point forward. + animation_mobjects = [anim.mobject for anim in animations] + mobjects = self.get_mobject_family_members() + for i, mob in enumerate(mobjects): + update_possibilities = [ + mob in animation_mobjects, + len(mob.get_family_updaters()) > 0, + mob in self.foreground_mobjects, + ] + if any(update_possibilities): + return mobjects[i:] + return [] + + def get_moving_and_static_mobjects(self, animations): + all_mobjects = list_update(self.mobjects, self.foreground_mobjects) + all_mobject_families = extract_mobject_family_members( + all_mobjects, + use_z_index=self.renderer.camera.use_z_index, + only_those_with_points=True, + ) + moving_mobjects = self.get_moving_mobjects(*animations) + all_moving_mobject_families = extract_mobject_family_members( + moving_mobjects, + use_z_index=self.renderer.camera.use_z_index, + ) + static_mobjects = list_difference_update( + all_mobject_families, + all_moving_mobject_families, + ) + return all_moving_mobject_families, static_mobjects + + def compile_animations( + self, + *args: Animation | Iterable[Animation] | types.GeneratorType[Animation], + **kwargs, + ): + """ + Creates _MethodAnimations from any _AnimationBuilders and updates animation + kwargs with kwargs passed to play(). + + Parameters + ---------- + *args + Animations to be played. + **kwargs + Configuration for the call to play(). + + Returns + ------- + Tuple[:class:`Animation`] + Animations to be played. + """ + animations = [] + arg_anims = flatten_iterable_parameters(args) + # Allow passing a generator to self.play instead of comma separated arguments + for arg in arg_anims: + try: + animations.append(prepare_animation(arg)) + except TypeError: + if inspect.ismethod(arg): + raise TypeError( + "Passing Mobject methods to Scene.play is no longer" + " supported. Use Mobject.animate instead.", + ) + else: + raise TypeError( + f"Unexpected argument {arg} passed to Scene.play().", + ) + + for animation in animations: + for k, v in kwargs.items(): + setattr(animation, k, v) + + return animations + + def _get_animation_time_progression( + self, animations: list[Animation], duration: float + ): + """ + You will hardly use this when making your own animations. + This method is for Manim's internal use. + + Uses :func:`~.get_time_progression` to obtain a + CommandLine ProgressBar whose ``fill_time`` is + dependent on the qualities of the passed Animation, + + Parameters + ---------- + animations + The list of animations to get + the time progression for. + + duration + duration of wait time + + Returns + ------- + time_progression + The CommandLine Progress Bar. + """ + if len(animations) == 1 and isinstance(animations[0], Wait): + stop_condition = animations[0].stop_condition + if stop_condition is not None: + time_progression = self.get_time_progression( + duration, + f"Waiting for {stop_condition.__name__}", + n_iterations=-1, # So it doesn't show % progress + override_skip_animations=True, + ) + else: + time_progression = self.get_time_progression( + duration, + f"Waiting {self.renderer.num_plays}", + ) + else: + time_progression = self.get_time_progression( + duration, + "".join( + [ + f"Animation {self.renderer.num_plays}: ", + str(animations[0]), + (", etc." if len(animations) > 1 else ""), + ], + ), + ) + return time_progression + + def get_time_progression( + self, + run_time: float, + description, + n_iterations: int | None = None, + override_skip_animations: bool = False, + ): + """ + You will hardly use this when making your own animations. + This method is for Manim's internal use. + + Returns a CommandLine ProgressBar whose ``fill_time`` + is dependent on the ``run_time`` of an animation, + the iterations to perform in that animation + and a bool saying whether or not to consider + the skipped animations. + + Parameters + ---------- + run_time + The ``run_time`` of the animation. + + n_iterations + The number of iterations in the animation. + + override_skip_animations + Whether or not to show skipped animations in the progress bar. + + Returns + ------- + time_progression + The CommandLine Progress Bar. + """ + if self.renderer.skip_animations and not override_skip_animations: + times = [run_time] + else: + step = 1 / config["frame_rate"] + times = np.arange(0, run_time, step) + time_progression = tqdm( + times, + desc=description, + total=n_iterations, + leave=config["progress_bar"] == "leave", + ascii=True if platform.system() == "Windows" else None, + disable=config["progress_bar"] == "none", + ) + return time_progression + + def get_run_time(self, animations: list[Animation]): + """ + Gets the total run time for a list of animations. + + Parameters + ---------- + animations + A list of the animations whose total + ``run_time`` is to be calculated. + + Returns + ------- + float + The total ``run_time`` of all of the animations in the list. + """ + + if len(animations) == 1 and isinstance(animations[0], Wait): + return animations[0].duration + + else: + return np.max([animation.run_time for animation in animations]) + + def play( + self, + *args: Animation | Iterable[Animation] | types.GeneratorType[Animation], + subcaption=None, + subcaption_duration=None, + subcaption_offset=0, + **kwargs, + ): + r"""Plays an animation in this scene. + + Parameters + ---------- + + args + Animations to be played. + subcaption + The content of the external subcaption that should + be added during the animation. + subcaption_duration + The duration for which the specified subcaption is + added. If ``None`` (the default), the run time of the + animation is taken. + subcaption_offset + An offset (in seconds) for the start time of the + added subcaption. + kwargs + All other keywords are passed to the renderer. + + """ + # If we are in interactive embedded mode, make sure this is running on the main thread (required for OpenGL) + if ( + self.interactive_mode + and config.renderer == RendererType.OPENGL + and threading.current_thread().name != "MainThread" + ): + kwargs.update( + { + "subcaption": subcaption, + "subcaption_duration": subcaption_duration, + "subcaption_offset": subcaption_offset, + } + ) + self.queue.put( + ( + "play", + args, + kwargs, + ) + ) + return + + start_time = self.renderer.time + self.renderer.play(self, *args, **kwargs) + run_time = self.renderer.time - start_time + if subcaption: + if subcaption_duration is None: + subcaption_duration = run_time + # The start of the subcaption needs to be offset by the + # run_time of the animation because it is added after + # the animation has already been played (and Scene.renderer.time + # has already been updated). + self.add_subcaption( + content=subcaption, + duration=subcaption_duration, + offset=-run_time + subcaption_offset, + ) + + def wait( + self, + duration: float = DEFAULT_WAIT_TIME, + stop_condition: Callable[[], bool] | None = None, + frozen_frame: bool | None = None, + ): + """Plays a "no operation" animation. + + Parameters + ---------- + duration + The run time of the animation. + stop_condition + A function without positional arguments that is evaluated every time + a frame is rendered. The animation only stops when the return value + of the function is truthy, or when the time specified in ``duration`` + passes. + frozen_frame + If True, updater functions are not evaluated, and the animation outputs + a frozen frame. If False, updater functions are called and frames + are rendered as usual. If None (the default), the scene tries to + determine whether or not the frame is frozen on its own. + + See also + -------- + :class:`.Wait`, :meth:`.should_mobjects_update` + """ + self.play( + Wait( + run_time=duration, + stop_condition=stop_condition, + frozen_frame=frozen_frame, + ) + ) + + def pause(self, duration: float = DEFAULT_WAIT_TIME): + """Pauses the scene (i.e., displays a frozen frame). + + This is an alias for :meth:`.wait` with ``frozen_frame`` + set to ``True``. + + Parameters + ---------- + duration + The duration of the pause. + + See also + -------- + :meth:`.wait`, :class:`.Wait` + """ + self.wait(duration=duration, frozen_frame=True) + + def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60): + """Wait until a condition is satisfied, up to a given maximum duration. + + Parameters + ---------- + stop_condition + A function with no arguments that determines whether or not the + scene should keep waiting. + max_time + The maximum wait time in seconds. + """ + self.wait(max_time, stop_condition=stop_condition) + + def compile_animation_data( + self, + *animations: Animation | Iterable[Animation] | types.GeneratorType[Animation], + **play_kwargs, + ): + """Given a list of animations, compile the corresponding + static and moving mobjects, and gather the animation durations. + + This also begins the animations. + + Parameters + ---------- + animations + Animation or mobject with mobject method and params + play_kwargs + Named parameters affecting what was passed in ``animations``, + e.g. ``run_time``, ``lag_ratio`` and so on. + + Returns + ------- + self, None + None if there is nothing to play, or self otherwise. + """ + # NOTE TODO : returns statement of this method are wrong. It should return nothing, as it makes a little sense to get any information from this method. + # The return are kept to keep webgl renderer from breaking. + if len(animations) == 0: + raise ValueError("Called Scene.play with no animations") + + self.animations = self.compile_animations(*animations, **play_kwargs) + self.add_mobjects_from_animations(self.animations) + + self.last_t = 0 + self.stop_condition = None + self.moving_mobjects = [] + self.static_mobjects = [] + + if len(self.animations) == 1 and isinstance(self.animations[0], Wait): + if self.should_update_mobjects(): + self.update_mobjects(dt=0) # Any problems with this? + self.stop_condition = self.animations[0].stop_condition + else: + self.duration = self.animations[0].duration + # Static image logic when the wait is static is done by the renderer, not here. + self.animations[0].is_static_wait = True + return None + self.duration = self.get_run_time(self.animations) + return self + + def begin_animations(self) -> None: + """Start the animations of the scene.""" + for animation in self.animations: + animation._setup_scene(self) + animation.begin() + + if config.renderer == RendererType.CAIRO: + # Paint all non-moving objects onto the screen, so they don't + # have to be rendered every frame + ( + self.moving_mobjects, + self.static_mobjects, + ) = self.get_moving_and_static_mobjects(self.animations) + + def is_current_animation_frozen_frame(self) -> bool: + """Returns whether the current animation produces a static frame (generally a Wait).""" + return ( + isinstance(self.animations[0], Wait) + and len(self.animations) == 1 + and self.animations[0].is_static_wait + ) + + def play_internal(self, skip_rendering: bool = False): + """ + This method is used to prep the animations for rendering, + apply the arguments and parameters required to them, + render them, and write them to the video file. + + Parameters + ---------- + skip_rendering + Whether the rendering should be skipped, by default False + """ + self.duration = self.get_run_time(self.animations) + self.time_progression = self._get_animation_time_progression( + self.animations, + self.duration, + ) + for t in self.time_progression: + self.update_to_time(t) + if not skip_rendering and not self.skip_animation_preview: + self.renderer.render(self, t, self.moving_mobjects) + if self.stop_condition is not None and self.stop_condition(): + self.time_progression.close() + break + + for animation in self.animations: + animation.finish() + animation.clean_up_from_scene(self) + if not self.renderer.skip_animations: + self.update_mobjects(0) + self.renderer.static_image = None + # Closing the progress bar at the end of the play. + self.time_progression.close() + + def check_interactive_embed_is_valid(self): + if config["force_window"]: + return True + if self.skip_animation_preview: + logger.warning( + "Disabling interactive embed as 'skip_animation_preview' is enabled", + ) + return False + elif config["write_to_movie"]: + logger.warning("Disabling interactive embed as 'write_to_movie' is enabled") + return False + elif config["format"]: + logger.warning( + "Disabling interactive embed as '--format' is set as " + + config["format"], + ) + return False + elif not self.renderer.window: + logger.warning("Disabling interactive embed as no window was created") + return False + elif config.dry_run: + logger.warning("Disabling interactive embed as dry_run is enabled") + return False + return True + + def interactive_embed(self): + """ + Like embed(), but allows for screen interaction. + """ + if not self.check_interactive_embed_is_valid(): + return + self.interactive_mode = True + + def ipython(shell, namespace): + import manim.opengl + + def load_module_into_namespace(module, namespace): + for name in dir(module): + namespace[name] = getattr(module, name) + + load_module_into_namespace(manim, namespace) + load_module_into_namespace(manim.opengl, namespace) + + def embedded_rerun(*args, **kwargs): + self.queue.put(("rerun_keyboard", args, kwargs)) + shell.exiter() + + namespace["rerun"] = embedded_rerun + + shell(local_ns=namespace) + self.queue.put(("exit_keyboard", [], {})) + + def get_embedded_method(method_name): + return lambda *args, **kwargs: self.queue.put((method_name, args, kwargs)) + + local_namespace = inspect.currentframe().f_back.f_locals + for method in ("play", "wait", "add", "remove"): + embedded_method = get_embedded_method(method) + # Allow for calling scene methods without prepending 'self.'. + local_namespace[method] = embedded_method + + from sqlite3 import connect + + from IPython.core.getipython import get_ipython + from IPython.terminal.embed import InteractiveShellEmbed + from traitlets.config import Config + + cfg = Config() + cfg.TerminalInteractiveShell.confirm_exit = False + if get_ipython() is None: + shell = InteractiveShellEmbed.instance(config=cfg) + else: + shell = InteractiveShellEmbed(config=cfg) + hist = get_ipython().history_manager + hist.db = connect(hist.hist_file, check_same_thread=False) + + keyboard_thread = threading.Thread( + target=ipython, + args=(shell, local_namespace), + ) + # run as daemon to kill thread when main thread exits + if not shell.pt_app: + keyboard_thread.daemon = True + keyboard_thread.start() + + if self.dearpygui_imported and config["enable_gui"]: + if not dpg.is_dearpygui_running(): + gui_thread = threading.Thread( + target=configure_pygui, + args=(self.renderer, self.widgets), + kwargs={"update": False}, + ) + gui_thread.start() + else: + configure_pygui(self.renderer, self.widgets, update=True) + + self.camera.model_matrix = self.camera.default_model_matrix + + self.interact(shell, keyboard_thread) + + def interact(self, shell, keyboard_thread): + event_handler = RerunSceneHandler(self.queue) + file_observer = Observer() + file_observer.schedule(event_handler, config["input_file"], recursive=True) + file_observer.start() + + self.quit_interaction = False + keyboard_thread_needs_join = shell.pt_app is not None + assert self.queue.qsize() == 0 + + last_time = time.time() + while not (self.renderer.window.is_closing or self.quit_interaction): + if not self.queue.empty(): + tup = self.queue.get_nowait() + if tup[0].startswith("rerun"): + # Intentionally skip calling join() on the file thread to save time. + if not tup[0].endswith("keyboard"): + if shell.pt_app: + shell.pt_app.app.exit(exception=EOFError) + file_observer.unschedule_all() + raise RerunSceneException + keyboard_thread.join() + + kwargs = tup[2] + if "from_animation_number" in kwargs: + config["from_animation_number"] = kwargs[ + "from_animation_number" + ] + # # TODO: This option only makes sense if interactive_embed() is run at the + # # end of a scene by default. + # if "upto_animation_number" in kwargs: + # config["upto_animation_number"] = kwargs[ + # "upto_animation_number" + # ] + + keyboard_thread.join() + file_observer.unschedule_all() + raise RerunSceneException + elif tup[0].startswith("exit"): + # Intentionally skip calling join() on the file thread to save time. + if not tup[0].endswith("keyboard") and shell.pt_app: + shell.pt_app.app.exit(exception=EOFError) + keyboard_thread.join() + # Remove exit_keyboard from the queue if necessary. + while self.queue.qsize() > 0: + self.queue.get() + keyboard_thread_needs_join = False + break + else: + method, args, kwargs = tup + getattr(self, method)(*args, **kwargs) + else: + self.renderer.animation_start_time = 0 + dt = time.time() - last_time + last_time = time.time() + self.renderer.render(self, dt, self.moving_mobjects) + self.update_mobjects(dt) + self.update_meshes(dt) + self.update_self(dt) + + # Join the keyboard thread if necessary. + if shell is not None and keyboard_thread_needs_join: + shell.pt_app.app.exit(exception=EOFError) + keyboard_thread.join() + # Remove exit_keyboard from the queue if necessary. + while self.queue.qsize() > 0: + self.queue.get() + + file_observer.stop() + file_observer.join() + + if self.dearpygui_imported and config["enable_gui"]: + dpg.stop_dearpygui() + + if self.renderer.window.is_closing: + self.renderer.window.destroy() + + def embed(self): + if not config["preview"]: + logger.warning("Called embed() while no preview window is available.") + return + if config["write_to_movie"]: + logger.warning("embed() is skipped while writing to a file.") + return + + self.renderer.animation_start_time = 0 + self.renderer.render(self, -1, self.moving_mobjects) + + # Configure IPython shell. + from IPython.terminal.embed import InteractiveShellEmbed + + shell = InteractiveShellEmbed() + + # Have the frame update after each command + shell.events.register( + "post_run_cell", + lambda *a, **kw: self.renderer.render(self, -1, self.moving_mobjects), + ) + + # Use the locals of the caller as the local namespace + # once embedded, and add a few custom shortcuts. + local_ns = inspect.currentframe().f_back.f_locals + # local_ns["touch"] = self.interact + for method in ( + "play", + "wait", + "add", + "remove", + "interact", + # "clear", + # "save_state", + # "restore", + ): + local_ns[method] = getattr(self, method) + shell(local_ns=local_ns, stack_depth=2) + + # End scene when exiting an embed. + raise Exception("Exiting scene.") + + def update_to_time(self, t): + dt = t - self.last_t + self.last_t = t + for animation in self.animations: + animation.update_mobjects(dt) + alpha = t / animation.run_time + animation.interpolate(alpha) + self.update_mobjects(dt) + self.update_meshes(dt) + self.update_self(dt) + + def add_subcaption( + self, content: str, duration: float = 1, offset: float = 0 + ) -> None: + r"""Adds an entry in the corresponding subcaption file + at the current time stamp. + + The current time stamp is obtained from ``Scene.renderer.time``. + + Parameters + ---------- + + content + The subcaption content. + duration + The duration (in seconds) for which the subcaption is shown. + offset + This offset (in seconds) is added to the starting time stamp + of the subcaption. + + Examples + -------- + + This example illustrates both possibilities for adding + subcaptions to Manimations:: + + class SubcaptionExample(Scene): + def construct(self): + square = Square() + circle = Circle() + + # first option: via the add_subcaption method + self.add_subcaption("Hello square!", duration=1) + self.play(Create(square)) + + # second option: within the call to Scene.play + self.play( + Transform(square, circle), + subcaption="The square transforms." + ) + + """ + subtitle = srt.Subtitle( + index=len(self.renderer.file_writer.subcaptions), + content=content, + start=datetime.timedelta(seconds=float(self.renderer.time + offset)), + end=datetime.timedelta( + seconds=float(self.renderer.time + offset + duration) + ), + ) + self.renderer.file_writer.subcaptions.append(subtitle) + + # 添加声音 + def add_sound( + self, + sound_file: str, + time_offset: float = 0, + gain: float | None = None, + **kwargs, + ): + """ + This method is used to add a sound to the animation. + + Parameters + ---------- + + sound_file + The path to the sound file. + time_offset + The offset in the sound file after which + the sound can be played. + gain + Amplification of the sound. + + Examples + -------- + .. manim:: SoundExample + :no_autoplay: + + class SoundExample(Scene): + # Source of sound under Creative Commons 0 License. https://freesound.org/people/Druminfected/sounds/250551/ + def construct(self): + dot = Dot().set_color(GREEN) + self.add_sound("click.wav") + self.add(dot) + self.wait() + self.add_sound("click.wav") + dot.set_color(BLUE) + self.wait() + self.add_sound("click.wav") + dot.set_color(RED) + self.wait() + + Download the resource for the previous example `here `_ . + """ + if self.renderer.skip_animations: + return + time = self.renderer.time + time_offset + self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs) + + # 处理鼠标移动事件 + def on_mouse_motion(self, point, d_point): + self.mouse_point.move_to(point) + if SHIFT_VALUE in self.renderer.pressed_keys: + shift = -d_point + shift[0] *= self.camera.get_width() / 2 + shift[1] *= self.camera.get_height() / 2 + transform = self.camera.inverse_rotation_matrix + shift = np.dot(np.transpose(transform), shift) + self.camera.shift(shift) + + # 处理鼠标滚轮事件 + def on_mouse_scroll(self, point, offset): + # 确定是否启用投影笔触着色器 + if not config.use_projection_stroke_shaders: + # 如果启用了这个功能,它会计算一个缩放因子 factor + factor = 1 + np.arctan(-2.1 * offset[1]) + # 以 self.camera_target 为中心缩放相机 + self.camera.scale(factor, about_point=self.camera_target) + # 处理鼠标滚轮控制 + self.mouse_scroll_orbit_controls(point, offset) + + # 定义键盘按下时的行为 + def on_key_press(self, symbol, modifiers): + # 尝试将按键符号转换为相应的字符 + try: + char = chr(symbol) + # 果按键符号太大而无法转换为字符,则记录一个警告消息,表示按键值过大。 + except OverflowError: + logger.warning("按键值过大!") + # 退出当前函数,避免继续执行后续代码 + return + # 按下'r'键可以将相机状态重置为默认状态,并将相机的目标位置设置为原点 + if char == "r": + self.camera.to_default_state() + self.camera_target = np.array([0, 0, 0], dtype=np.float32) + # 按下'q'键可以退出交互模式 + elif char == "q": + self.quit_interaction = True + # 如果按下的键有特定的函数映射到它,那么将执行该函数 + else: + if char in self.key_to_function_map: + self.key_to_function_map[char]() + + # 定义键盘松开时的行为 + def on_key_release(self, symbol, modifiers): + pass + + # 定义鼠标拖拽时的行为 + def on_mouse_drag(self, point, d_point, buttons, modifiers): + self.mouse_drag_point.move_to(point) + # 鼠标左键按下时(buttons == 1),相机会根据鼠标的水平和垂直移动改变水平角度(theta)和垂直角度(phi) + if buttons == 1: + self.camera.increment_theta(-d_point[0]) + self.camera.increment_phi(d_point[1]) + # 鼠标右键按下时(buttons == 4),相机会沿着相机自身的X轴和与OUT方向的叉乘方向移动,以实现平移视角的效果 + elif buttons == 4: + camera_x_axis = self.camera.model_matrix[:3, 0] + horizontal_shift_vector = -d_point[0] * camera_x_axis + vertical_shift_vector = -d_point[1] * np.cross(OUT, camera_x_axis) + total_shift_vector = horizontal_shift_vector + vertical_shift_vector + self.camera.shift(1.1 * total_shift_vector) + self.mouse_drag_orbit_controls(point, d_point, buttons, modifiers) + + # 处理鼠标滚轮控制相机轨道运动,根据鼠标滚轮的偏移量offset来确定相机沿着视线方向移动的距离 + def mouse_scroll_orbit_controls(self, point, offset): + # 首先计算相机到目标点的向量 + camera_to_target = self.camera_target - self.camera.get_position() + # 滚轮向上滚动为正,向下滚动为负,调整这个向量的方向 + camera_to_target *= np.sign(offset[1]) + # 乘以一个固定的缩放因子 + shift_vector = 0.01 * camera_to_target + # 将相机的模型矩阵应用这个位移来实现相机的移动 + self.camera.model_matrix = ( + opengl.translation_matrix(*shift_vector) @ self.camera.model_matrix + ) + + # 鼠标拖动控制相机,允许用户通过鼠标左键和右键进行不同的操作 + def mouse_drag_orbit_controls(self, point, d_point, buttons, modifiers): + # 鼠标左键拖动时,相机围绕目标点旋转 + if buttons == 1: + # 将操作点转换为以原点为中心,围绕 z 轴旋转 + self.camera.model_matrix = ( + # 围绕 z 轴以 d_point[0] 的负值为角度进行旋转。这里的 d_point[0] 可能是鼠标在 x 方向上的拖动距离 + opengl.rotation_matrix(z=-d_point[0]) + # 将相机位置平移到目标点的负值位置。这样可以使相机绕目标点旋转,而不是围绕自己的原点 + @ opengl.translation_matrix(*-self.camera_target) + # 将上述旋转和平移操作应用到相机的模型矩阵上,从而改变相机的位置和方向 + @ self.camera.model_matrix + ) + # 处理鼠标拖拽导致的相机绕特定轴的旋转 + # 获取相机当前的位置 + camera_position = self.camera.get_position() + # 获取相机当前的 y 轴方向,即相机模型矩阵的第二列(索引为 1) + camera_y_axis = self.camera.model_matrix[:3, 1] + # 计算旋转轴,即相机的 y 轴和相机位置向量的叉乘结果,然后进行归一化处理 + axis_of_rotation = space_ops.normalize( + np.cross(camera_y_axis, camera_position), + ) + # 根据鼠标在垂直方向上的拖拽量 d_point[1] 和旋转轴 axis_of_rotation,生成一个绕该轴旋转的旋转矩阵 + rotation_matrix = space_ops.rotation_matrix( + d_point[1], + axis_of_rotation, + homogeneous=True, + ) + # 获取相机的最大极角和最小极角,限制相机绕某些轴的旋转范围,以确保相机的视角不会超出特定范围 + maximum_polar_angle = self.camera.maximum_polar_angle + minimum_polar_angle = self.camera.minimum_polar_angle + # 计算了潜在的相机位置和旋转后的模型矩阵 + potential_camera_model_matrix = rotation_matrix @ self.camera.model_matrix + potential_camera_location = potential_camera_model_matrix[:3, 3] + potential_camera_y_axis = potential_camera_model_matrix[:3, 1] + # 确定角度符号的值,保证了sign的值在potential_camera_y_axis[2]为零时仍然有效,避免了除以零的错误 + sign = ( + np.sign(potential_camera_y_axis[2]) + if potential_camera_y_axis[2] != 0 + else 1 + ) + # 计算了潜在的极角,用于确定相机绕其观察目标点的旋转角度 + potential_polar_angle = sign * np.arccos( + potential_camera_location[2] + / np.linalg.norm(potential_camera_location), + ) + # 检查这个极角是否在允许的范围内 + if minimum_polar_angle <= potential_polar_angle <= maximum_polar_angle: + # 如果在范围内,则直接更新相机的模型矩阵为潜在的模型矩阵 + self.camera.model_matrix = potential_camera_model_matrix + # 如果不在范围内,则计算需要旋转的角度,并重新计算旋转矩阵并应用到相机的模型矩阵上 + else: + sign = np.sign(camera_y_axis[2]) if camera_y_axis[2] != 0 else 1 + current_polar_angle = sign * np.arccos( + camera_position[2] / np.linalg.norm(camera_position), + ) + if potential_polar_angle > maximum_polar_angle: + polar_angle_delta = maximum_polar_angle - current_polar_angle + else: + polar_angle_delta = minimum_polar_angle - current_polar_angle + rotation_matrix = space_ops.rotation_matrix( + polar_angle_delta, + axis_of_rotation, + homogeneous=True, + ) + self.camera.model_matrix = rotation_matrix @ self.camera.model_matrix + # 将相机平移回原始的目标点位置,以保持相机视线方向的稳定 + self.camera.model_matrix = ( + # 根据相机的目标点位置 self.camera_target,创建一个平移矩阵,使得相机的目标点移动到世界坐标系的原点位置 + opengl.translation_matrix(*self.camera_target) + # 与当前的相机模型矩阵相乘 + @ self.camera.model_matrix + ) + # 鼠标右键拖动时,相机在平面上的平移 + elif buttons == 4: + # 获取相机模型矩阵的第一列,即相机的 x 轴方向 + camera_x_axis = self.camera.model_matrix[:3, 0] + # 根据鼠标水平方向上的移动距离 d_point[0],计算出相机需要水平平移的向量,即相机 x 轴方向上的移动 + horizontal_shift_vector = -d_point[0] * camera_x_axis + # 根据鼠标垂直方向上的移动距离 d_point[1],通过计算相机的 y 轴方向与世界坐标系的 z 轴方向的叉乘,计算出相机需要垂直平移的向量,即相机 y 轴方向上的移动 + vertical_shift_vector = -d_point[1] * np.cross(OUT, camera_x_axis) + # 将水平和垂直方向上的移动向量相加,得到总的平移向量 + total_shift_vector = horizontal_shift_vector + vertical_shift_vector + # 将总的平移向量应用到相机的模型矩阵上,实现相机的平移操作 + self.camera.model_matrix = ( + opengl.translation_matrix(*total_shift_vector) + @ self.camera.model_matrix + ) + # 同时更新相机的目标点,保持相机视线的方向不变,但相机的位置发生了变化,从而实现平移效果 + self.camera_target += total_shift_vector + + # 将按键与函数关联起来,当按下特定按键时,将执行相应的函数 + def set_key_function(self, char, func): + self.key_to_function_map[char] = func + + # 处理鼠标按下事件,依次调用已注册的鼠标按下回调函数 + def on_mouse_press(self, point, button, modifiers): + for func in self.mouse_press_callbacks: + func() From 48d4f7328d70aa68bb0d597b0a4a8fb38d80a537 Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Mon, 19 Feb 2024 11:53:26 +0800 Subject: [PATCH 25/26] Update *scene.py --- manimlib/scene/*scene.py | 251 +++++++++++++++------------------------ 1 file changed, 95 insertions(+), 156 deletions(-) diff --git a/manimlib/scene/*scene.py b/manimlib/scene/*scene.py index 44e8bc8206..54defd382e 100644 --- a/manimlib/scene/*scene.py +++ b/manimlib/scene/*scene.py @@ -156,16 +156,19 @@ def __init__( self.renderer.init_scene(self) self.mobjects = [] - # TODO, remove need for foreground mobjects + # 待办, 消除对前台对象的需要 self.foreground_mobjects = [] + # 如果提供了random_seed,则会使用该种子来初始化Python的random模块和numpy库的随机数生成器 + # 这可以确保在每次运行时生成的随机数序列相同,从而使得随机性可复现 if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) - @property + @property # 用于获取renderer.camera属性的装饰器,可以将一个方法定义为类的属性,使得在访问这个方法时可以像访问属性一样,不需要使用()来调用方法 def camera(self): return self.renderer.camera + # 深拷贝 def __deepcopy__(self, clone_from_id): cls = self.__class__ result = cls.__new__(cls) @@ -344,6 +347,7 @@ def get_attrs(self, *keys: str): """ return [getattr(self, key) for key in keys] + # 更新场景中的所有物体 def update_mobjects(self, dt: float): """ Begins updating all mobjects in the Scene. @@ -880,6 +884,8 @@ def get_moving_and_static_mobjects(self, animations): ) return all_moving_mobject_families, static_mobjects + # 将传入的动画参数转换为适用于播放的动画对象 + # 从动画构建器中创建动画方法并更新动画 def compile_animations( self, *args: Animation | Iterable[Animation] | types.GeneratorType[Animation], @@ -910,8 +916,7 @@ def compile_animations( except TypeError: if inspect.ismethod(arg): raise TypeError( - "Passing Mobject methods to Scene.play is no longer" - " supported. Use Mobject.animate instead.", + "向Scene.play中传递物体方法已经被弃用。请使用Mobject.animate。", ) else: raise TypeError( @@ -1024,6 +1029,7 @@ def get_time_progression( ) return time_progression + # 获取动画列表的总运行时间 def get_run_time(self, animations: list[Animation]): """ Gets the total run time for a list of animations. @@ -1046,36 +1052,20 @@ def get_run_time(self, animations: list[Animation]): else: return np.max([animation.run_time for animation in animations]) + # 在此场景中播放动画 def play( self, + # 动画对象或动画对象的可迭代集合,用于指定要播放的动画 *args: Animation | Iterable[Animation] | types.GeneratorType[Animation], + # 要添加的外部子标题的内容 subcaption=None, + # 添加的子标题显示时间。如果为None,则取动画的运行时间 subcaption_duration=None, + # 子标题的开始时间的偏移量(以秒为单位) subcaption_offset=0, **kwargs, ): - r"""Plays an animation in this scene. - - Parameters - ---------- - - args - Animations to be played. - subcaption - The content of the external subcaption that should - be added during the animation. - subcaption_duration - The duration for which the specified subcaption is - added. If ``None`` (the default), the run time of the - animation is taken. - subcaption_offset - An offset (in seconds) for the start time of the - added subcaption. - kwargs - All other keywords are passed to the renderer. - - """ - # If we are in interactive embedded mode, make sure this is running on the main thread (required for OpenGL) + # 如果在交互式嵌入模式下,并且渲染器为OpenGL,则需要确保此方法在主线程上运行(这对于OpenGL是必需的) if ( self.interactive_mode and config.renderer == RendererType.OPENGL @@ -1088,6 +1078,7 @@ def play( "subcaption_offset": subcaption_offset, } ) + # 如果不是主线程,则将任务放入队列以在主线程上运行,并且更新关键字参数以包含子标题相关信息 self.queue.put( ( "play", @@ -1100,46 +1091,33 @@ def play( start_time = self.renderer.time self.renderer.play(self, *args, **kwargs) run_time = self.renderer.time - start_time + # 如果提供了subcaption参数,则在动画播放后添加子标题,其开始时间需要根据动画的运行时间进行偏移 if subcaption: + # 检查是否指定了子标题的持续时间 if subcaption_duration is None: + # 如果没有指定持续时间,则默认为动画的运行时间 subcaption_duration = run_time - # The start of the subcaption needs to be offset by the - # run_time of the animation because it is added after - # the animation has already been played (and Scene.renderer.time - # has already been updated). + # 将子标题添加到当前场景 + # 需要注意的是,子标题的开始时间需要根据动画的运行时间做相应的偏移,以确保子标题在适当的时机显示出来 self.add_subcaption( content=subcaption, duration=subcaption_duration, offset=-run_time + subcaption_offset, ) + # 播放一个“无操作”的动画,即暂停执行并显示静止帧 def wait( self, + # 动画的运行时间 duration: float = DEFAULT_WAIT_TIME, + # 无参函数,用于在每次渲染帧时评估 + # 只有当函数的返回值为真时,或者超过 duration 指定的时间时,动画才会停止 stop_condition: Callable[[], bool] | None = None, + # 控制是否冻结帧。如果为 True,则更新器函数不会被调用,动画输出一个静止的帧 + # 如果为 False,则更新器函数会被调用,帧会像平常一样被渲染 + # 如果为 None(默认值),则场景会尝试自行确定帧是否应该被冻结 frozen_frame: bool | None = None, ): - """Plays a "no operation" animation. - - Parameters - ---------- - duration - The run time of the animation. - stop_condition - A function without positional arguments that is evaluated every time - a frame is rendered. The animation only stops when the return value - of the function is truthy, or when the time specified in ``duration`` - passes. - frozen_frame - If True, updater functions are not evaluated, and the animation outputs - a frozen frame. If False, updater functions are called and frames - are rendered as usual. If None (the default), the scene tries to - determine whether or not the frame is frozen on its own. - - See also - -------- - :class:`.Wait`, :meth:`.should_mobjects_update` - """ self.play( Wait( run_time=duration, @@ -1148,34 +1126,21 @@ def wait( ) ) + # 暂停场景(即显示冻结的帧) def pause(self, duration: float = DEFAULT_WAIT_TIME): - """Pauses the scene (i.e., displays a frozen frame). - - This is an alias for :meth:`.wait` with ``frozen_frame`` - set to ``True``. - - Parameters - ---------- - duration - The duration of the pause. - - See also - -------- - :meth:`.wait`, :class:`.Wait` - """ + # 实际上是 wait 方法的一个别名,只是在调用 wait 方法时将 frozen_frame 参数设置为 True + # 这样,调用 pause 方法时就会等同于调用 wait 方法并指定 frozen_frame=True + # 参数 duration 指定了暂停的持续时间,即静止帧的显示时间 self.wait(duration=duration, frozen_frame=True) - def wait_until(self, stop_condition: Callable[[], bool], max_time: float = 60): - """Wait until a condition is satisfied, up to a given maximum duration. - - Parameters - ---------- - stop_condition - A function with no arguments that determines whether or not the - scene should keep waiting. - max_time - The maximum wait time in seconds. - """ + # 等待直到满足条件,直至达到给定的最大持续时间 + def wait_until( + self, + # 无参函数,用于确定场景是否应该继续等待 + stop_condition: Callable[[], bool], + # 最大等待时间,以秒为单位 + max_time: float = 60 + ): self.wait(max_time, stop_condition=stop_condition) def compile_animation_data( @@ -1458,7 +1423,9 @@ def interact(self, shell, keyboard_thread): if self.renderer.window.is_closing: self.renderer.window.destroy() + # 在 Manim 中嵌入 IPython 交互式 shell 的功能 def embed(self): + # 检查当前是否处于预览模式(即未将动画渲染到文件)以及 IPython 是否可用 if not config["preview"]: logger.warning("Called embed() while no preview window is available.") return @@ -1466,22 +1433,22 @@ def embed(self): logger.warning("embed() is skipped while writing to a file.") return + # 设置动画的起始时间为 0 self.renderer.animation_start_time = 0 self.renderer.render(self, -1, self.moving_mobjects) - # Configure IPython shell. + # 创建了一个 IPython 的交互式 shell from IPython.terminal.embed import InteractiveShellEmbed - shell = InteractiveShellEmbed() - # Have the frame update after each command + # 在 IPython shell 中,每次运行单元格后,都会调用 render 方法来更新动画 shell.events.register( "post_run_cell", lambda *a, **kw: self.renderer.render(self, -1, self.moving_mobjects), ) - # Use the locals of the caller as the local namespace - # once embedded, and add a few custom shortcuts. + # 将一些 Manim 中常用的方法添加到了 IPython shell 的本地命名空间中 + # 这样用户可以直接在 IPython 中调用这些方法 local_ns = inspect.currentframe().f_back.f_locals # local_ns["touch"] = self.interact for method in ( @@ -1497,9 +1464,10 @@ def embed(self): local_ns[method] = getattr(self, method) shell(local_ns=local_ns, stack_depth=2) - # End scene when exiting an embed. + # 当退出嵌入式环境时,通过引发异常来结束场景 raise Exception("Exiting scene.") + # 更新场景到指定的时间点 def update_to_time(self, t): dt = t - self.last_t self.last_t = t @@ -1507,106 +1475,77 @@ def update_to_time(self, t): animation.update_mobjects(dt) alpha = t / animation.run_time animation.interpolate(alpha) + # 更新物体 self.update_mobjects(dt) + # 更新网格 self.update_meshes(dt) + # 更新场景本身 self.update_self(dt) def add_subcaption( self, content: str, duration: float = 1, offset: float = 0 ) -> None: - r"""Adds an entry in the corresponding subcaption file - at the current time stamp. - - The current time stamp is obtained from ``Scene.renderer.time``. - - Parameters - ---------- - - content - The subcaption content. - duration - The duration (in seconds) for which the subcaption is shown. - offset - This offset (in seconds) is added to the starting time stamp - of the subcaption. - - Examples - -------- - - This example illustrates both possibilities for adding - subcaptions to Manimations:: - - class SubcaptionExample(Scene): - def construct(self): - square = Square() - circle = Circle() - - # first option: via the add_subcaption method - self.add_subcaption("Hello square!", duration=1) - self.play(Create(square)) - - # second option: within the call to Scene.play - self.play( - Transform(square, circle), - subcaption="The square transforms." - ) - - """ + # 在相应的子字幕文件中添加一个包含当前时间戳的条目 + # 当前时间戳是在``Scene.renderer.time``中获得的 + # 此示例说明了向动画添加子标题的两种可能性: + # class SubcaptionExample(Scene): + # def construct(self): + # square = Square() + # circle = Circle() + + # 法一: 通过 add_subcaption + # self.add_subcaption("Hello square!", duration=1) + # self.play(Create(square)) + + # 法二: 在对 Scene.play 的调用中 + # self.play( + # Transform(square, circle), + # subcaption="The square transforms." + # ) subtitle = srt.Subtitle( index=len(self.renderer.file_writer.subcaptions), + # 要添加的字幕内容 content=content, + # 开始时间 start=datetime.timedelta(seconds=float(self.renderer.time + offset)), + # 结束时间 end=datetime.timedelta( seconds=float(self.renderer.time + offset + duration) ), ) + # 将这个对象添加到场景的字幕列表中 self.renderer.file_writer.subcaptions.append(subtitle) - # 添加声音 + # 向动画中添加声音 def add_sound( self, + # 声音文件的路径 sound_file: str, + # 声音文件中的偏移时间 time_offset: float = 0, + # 增益参数,用于放大声音 gain: float | None = None, **kwargs, ): - """ - This method is used to add a sound to the animation. - - Parameters - ---------- - - sound_file - The path to the sound file. - time_offset - The offset in the sound file after which - the sound can be played. - gain - Amplification of the sound. - - Examples - -------- - .. manim:: SoundExample - :no_autoplay: - - class SoundExample(Scene): - # Source of sound under Creative Commons 0 License. https://freesound.org/people/Druminfected/sounds/250551/ - def construct(self): - dot = Dot().set_color(GREEN) - self.add_sound("click.wav") - self.add(dot) - self.wait() - self.add_sound("click.wav") - dot.set_color(BLUE) - self.wait() - self.add_sound("click.wav") - dot.set_color(RED) - self.wait() - - Download the resource for the previous example `here `_ . - """ + # 示例(非自动播放) + # class SoundExample(Scene): + # def construct(self): + # dot = Dot().set_color(GREEN) + # self.add_sound("click.wav") + # self.add(dot) + # self.wait() + # self.add_sound("click.wav") + # dot.set_color(BLUE) + # self.wait() + # self.add_sound("click.wav") + # dot.set_color(RED) + # self.wait() + + # 声音源文件地址 https://github.com/ManimCommunity/manim/blob/main/docs/source/_static/click.wav + # 检查是否应该跳过动画渲染 if self.renderer.skip_animations: return + # 计算出声音应该播放的时间点(当前渲染时间加上偏移量) time = self.renderer.time + time_offset self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs) From 0dc97b7c5e0f7b4ce4c89dfc44a1df4d24a1431e Mon Sep 17 00:00:00 2001 From: LiJY136688 <150437199+LiJY136688@users.noreply.github.com> Date: Mon, 19 Feb 2024 18:45:20 +0800 Subject: [PATCH 26/26] Create *geometry.arc.py --- manimlib/mobject/*geometry.arc.py | 1263 +++++++++++++++++++++++++++++ 1 file changed, 1263 insertions(+) create mode 100644 manimlib/mobject/*geometry.arc.py diff --git a/manimlib/mobject/*geometry.arc.py b/manimlib/mobject/*geometry.arc.py new file mode 100644 index 0000000000..4ca0bf3779 --- /dev/null +++ b/manimlib/mobject/*geometry.arc.py @@ -0,0 +1,1263 @@ +# 曲线物体 + +# 一个简单的圆点 Dot (m0) +# 一个带有注释的圆点 AnnotationDot (m1) +# 一个带有标签的圆点 LabeledDot (m2) +# 一个带有数学公式标签的圆点 LabeledDot (m3) +# 一个曲线箭头 CurvedArrow (m4) +# 一个带有负半径的曲线箭头 CurvedArrow (m5) +# 一个双曲线箭头 CurvedDoubleArrow (m6) + +# class UsefulAnnotations(Scene): +# def construct(self): +# m0 = Dot() +# m1 = AnnotationDot() +# m2 = LabeledDot("ii") +# m3 = LabeledDot(MathTex(r"\alpha").set_color(ORANGE)) +# m4 = CurvedArrow(2*LEFT, 2*RIGHT, radius= -5) +# m5 = CurvedArrow(2*LEFT, 2*RIGHT, radius= 8) +# m6 = CurvedDoubleArrow(ORIGIN, 2*RIGHT) +# 然后将这些对象添加到场景中,并通过循环将它们沿垂直方向向下移动,以便在屏幕上垂直排列。 +# self.add(m0, m1, m2, m3, m4, m5, m6) +# for i, mobj in enumerate(self.mobjects): +# mobj.shift(DOWN * (i-3)) + +from __future__ import annotations + +# 当使用类似from module_name import *的语法时,只会导入__all__列表中指定的符号,而不会导入其他未在列表中指定的符号 +# 这样可以避免导入过多的符号 +__all__ = [ + "TipableVMobject", + "Arc", + "ArcBetweenPoints", + "CurvedArrow", + "CurvedDoubleArrow", + "Circle", + "Dot", + "AnnotationDot", + "LabeledDot", + "Ellipse", + "AnnularSector", + "Sector", + "Annulus", + "CubicBezier", + "ArcPolygon", + "ArcPolygonFromArcs", +] + + +import itertools +import warnings +from typing import TYPE_CHECKING + +import numpy as np +from typing_extensions import Self + +from manim.constants import * +from manim.mobject.opengl.opengl_compatibility import ConvertToOpenGL +from manim.mobject.types.vectorized_mobject import VGroup, VMobject +from manim.utils.color import BLACK, BLUE, RED, WHITE, ParsableManimColor +from manim.utils.iterables import adjacent_pairs +from manim.utils.space_ops import ( + angle_of_vector, + cartesian_to_spherical, + line_intersection, + perpendicular_bisector, + rotate_vector, +) + +if TYPE_CHECKING: + import manim.mobject.geometry.tips as tips + from manim.mobject.mobject import Mobject + from manim.mobject.text.tex_mobject import SingleStringMathTex, Tex + from manim.mobject.text.text_mobject import Text + from manim.typing import CubicBezierPoints, Point3D, QuadraticBezierPoints, Vector + + + +# 这些常量定义了默认的点和箭头的尺寸参数: +# DEFAULT_DOT_RADIUS:默认点的半径大小为 0.08。 +# DEFAULT_SMALL_DOT_RADIUS:默认小点的半径大小为 0.04。 +# DEFAULT_DASH_LENGTH:默认虚线的长度为 0.05。 +# DEFAULT_ARROW_TIP_LENGTH:默认箭头的长度为 0.35。 +# DEFAULT_ARROW_TIP_WIDTH:默认箭头的宽度为 0.35。 +DEFAULT_DOT_RADIUS = 0.08 +DEFAULT_SMALL_DOT_RADIUS = 0.04 +DEFAULT_DASH_LENGTH = 0.05 +DEFAULT_ARROW_TIP_LENGTH = 0.35 +DEFAULT_ARROW_TIP_WIDTH = 0.35 + +# +class TipableVMobject(VMobject, metaclass=ConvertToOpenGL): + """Meant for shared functionality between Arc and Line. + Functionality can be classified broadly into these groups: + + 添加、创建、修改 tips + add_tip 方法调用 create_tip 方法, 在将新 tip 推入 TipableVMobject 的子对象列表之前 + 进行风格和位置配置 + + 检查 tips + Boolean checks for whether the TipableVMobject has a tip and a starting tip + + 获取器 + Straightforward accessors, returning information pertaining to the TipableVMobject instance's tip(s), its length etc + """ + + def __init__( + self, + tip_length: float = DEFAULT_ARROW_TIP_LENGTH, + normal_vector: Vector = OUT, + tip_style: dict = {}, + **kwargs, + ) -> None: + self.tip_length: float = tip_length + self.normal_vector: Vector = normal_vector + self.tip_style: dict = tip_style + super().__init__(**kwargs) + + # Adding, Creating, Modifying tips + + def add_tip( + self, + tip: tips.ArrowTip | None = None, + tip_shape: type[tips.ArrowTip] | None = None, + tip_length: float | None = None, + tip_width: float | None = None, + at_start: bool = False, + ) -> Self: + # Adds a tip to the TipableVMobject instance, recognising that the endpoints might need to be switched if it's a 'starting tip' or not. + if tip is None: + tip = self.create_tip(tip_shape, tip_length, tip_width, at_start) + else: + self.position_tip(tip, at_start) + self.reset_endpoints_based_on_tip(tip, at_start) + self.asign_tip_attr(tip, at_start) + self.add(tip) + return self + + def create_tip( + self, + tip_shape: type[tips.ArrowTip] | None = None, + tip_length: float = None, + tip_width: float = None, + at_start: bool = False, + ): + """Stylises the tip, positions it spatially, and returns + the newly instantiated tip to the caller. + """ + tip = self.get_unpositioned_tip(tip_shape, tip_length, tip_width) + self.position_tip(tip, at_start) + return tip + + def get_unpositioned_tip( + self, + tip_shape: type[tips.ArrowTip] | None = None, + tip_length: float | None = None, + tip_width: float | None = None, + ): + """Returns a tip that has been stylistically configured, + but has not yet been given a position in space. + """ + from manim.mobject.geometry.tips import ArrowTriangleFilledTip + + style = {} + + if tip_shape is None: + tip_shape = ArrowTriangleFilledTip + + if tip_shape is ArrowTriangleFilledTip: + if tip_width is None: + tip_width = self.get_default_tip_length() + style.update({"width": tip_width}) + if tip_length is None: + tip_length = self.get_default_tip_length() + + color = self.get_color() + style.update({"fill_color": color, "stroke_color": color}) + style.update(self.tip_style) + tip = tip_shape(length=tip_length, **style) + return tip + + def position_tip(self, tip: tips.ArrowTip, at_start: bool = False): + # Last two control points, defining both + # the end, and the tangency direction + if at_start: + anchor = self.get_start() + handle = self.get_first_handle() + else: + handle = self.get_last_handle() + anchor = self.get_end() + angles = cartesian_to_spherical(handle - anchor) + tip.rotate( + angles[1] - PI - tip.tip_angle, + ) # Rotates the tip along the azimuthal + if not hasattr(self, "_init_positioning_axis"): + axis = [ + np.sin(angles[1]), + -np.cos(angles[1]), + 0, + ] # Obtains the perpendicular of the tip + tip.rotate( + -angles[2] + PI / 2, + axis=axis, + ) # Rotates the tip along the vertical wrt the axis + self._init_positioning_axis = axis + tip.shift(anchor - tip.tip_point) + return tip + + def reset_endpoints_based_on_tip(self, tip: tips.ArrowTip, at_start: bool) -> Self: + if self.get_length() == 0: + # Zero length, put_start_and_end_on wouldn't work + return self + + if at_start: + self.put_start_and_end_on(tip.base, self.get_end()) + else: + self.put_start_and_end_on(self.get_start(), tip.base) + return self + + def asign_tip_attr(self, tip: tips.ArrowTip, at_start: bool) -> Self: + if at_start: + self.start_tip = tip + else: + self.tip = tip + return self + + # Checking for tips + + def has_tip(self) -> bool: + return hasattr(self, "tip") and self.tip in self + + def has_start_tip(self) -> bool: + return hasattr(self, "start_tip") and self.start_tip in self + + # Getters + + # 移除并返回 TipableVMobject 实例中的箭头(tip) + def pop_tips(self) -> VGroup: + # 获取 TipableVMobject 实例的起始点和终点 + start, end = self.get_start_and_end() + # 存储被移除的箭头 + result = self.get_group_class()() + # 如果 TipableVMobject 实例有箭头,将箭头添加到 result 中,并从实例中移除箭头 + if self.has_tip(): + result.add(self.tip) + self.remove(self.tip) + # # 如果 TipableVMobject 实例有起始箭头,将起始箭头添加到 result 中,并从实例中移除起始箭头 + if self.has_start_tip(): + result.add(self.start_tip) + self.remove(self.start_tip) + # 最后,将起始点和终点重新放置到原来的位置,并返回存储了被移除箭头的 result + self.put_start_and_end_on(start, end) + return result + + def get_tips(self) -> VGroup: + """Returns a VGroup (collection of VMobjects) containing + the TipableVMObject instance's tips. + """ + result = self.get_group_class()() + if hasattr(self, "tip"): + result.add(self.tip) + if hasattr(self, "start_tip"): + result.add(self.start_tip) + return result + + def get_tip(self): + """Returns the TipableVMobject instance's (first) tip, + otherwise throws an exception.""" + tips = self.get_tips() + if len(tips) == 0: + raise Exception("tip not found") + else: + return tips[0] + + def get_default_tip_length(self) -> float: + return self.tip_length + + def get_first_handle(self) -> Point3D: + return self.points[1] + + def get_last_handle(self) -> Point3D: + return self.points[-2] + + def get_end(self) -> Point3D: + if self.has_tip(): + return self.tip.get_start() + else: + return super().get_end() + + def get_start(self) -> Point3D: + if self.has_start_tip(): + return self.start_tip.get_start() + else: + return super().get_start() + + def get_length(self) -> np.floating: + start, end = self.get_start_and_end() + return np.linalg.norm(start - end) + + +class Arc(TipableVMobject): + """A circular arc. + + Examples + -------- + A simple arc of angle Pi. + + .. manim:: ArcExample + :save_last_frame: + + class ArcExample(Scene): + def construct(self): + self.add(Arc(angle=PI)) + """ + + def __init__( + self, + radius: float = 1.0, + start_angle: float = 0, + angle: float = TAU / 4, + num_components: int = 9, + arc_center: Point3D = ORIGIN, + **kwargs, + ): + if radius is None: # apparently None is passed by ArcBetweenPoints + radius = 1.0 + self.radius = radius + self.num_components: int = num_components + self.arc_center: Point3D = arc_center + self.start_angle: float = start_angle + self.angle: float = angle + self._failed_to_get_center: bool = False + super().__init__(**kwargs) + + def generate_points(self) -> None: + self._set_pre_positioned_points() + self.scale(self.radius, about_point=ORIGIN) + self.shift(self.arc_center) + + # Points are set a bit differently when rendering via OpenGL. + # TODO: refactor Arc so that only one strategy for setting points + # has to be used. + def init_points(self) -> None: + self.set_points( + Arc._create_quadratic_bezier_points( + angle=self.angle, + start_angle=self.start_angle, + n_components=self.num_components, + ), + ) + self.scale(self.radius, about_point=ORIGIN) + self.shift(self.arc_center) + + @staticmethod + def _create_quadratic_bezier_points( + angle: float, start_angle: float = 0, n_components: int = 8 + ) -> QuadraticBezierPoints: + samples = np.array( + [ + [np.cos(a), np.sin(a), 0] + for a in np.linspace( + start_angle, + start_angle + angle, + 2 * n_components + 1, + ) + ], + ) + theta = angle / n_components + samples[1::2] /= np.cos(theta / 2) + + points = np.zeros((3 * n_components, 3)) + points[0::3] = samples[0:-1:2] + points[1::3] = samples[1::2] + points[2::3] = samples[2::2] + return points + + def _set_pre_positioned_points(self) -> None: + anchors = np.array( + [ + np.cos(a) * RIGHT + np.sin(a) * UP + for a in np.linspace( + self.start_angle, + self.start_angle + self.angle, + self.num_components, + ) + ], + ) + # Figure out which control points will give the + # Appropriate tangent lines to the circle + d_theta = self.angle / (self.num_components - 1.0) + tangent_vectors = np.zeros(anchors.shape) + # Rotate all 90 degrees, via (x, y) -> (-y, x) + tangent_vectors[:, 1] = anchors[:, 0] + tangent_vectors[:, 0] = -anchors[:, 1] + # Use tangent vectors to deduce anchors + handles1 = anchors[:-1] + (d_theta / 3) * tangent_vectors[:-1] + handles2 = anchors[1:] - (d_theta / 3) * tangent_vectors[1:] + self.set_anchors_and_handles(anchors[:-1], handles1, handles2, anchors[1:]) + + def get_arc_center(self, warning: bool = True) -> Point3D: + """Looks at the normals to the first two + anchors, and finds their intersection points + """ + # First two anchors and handles + a1, h1, h2, a2 = self.points[:4] + + if np.all(a1 == a2): + # For a1 and a2 to lie at the same point arc radius + # must be zero. Thus arc_center will also lie at + # that point. + return a1 + # Tangent vectors + t1 = h1 - a1 + t2 = h2 - a2 + # Normals + n1 = rotate_vector(t1, TAU / 4) + n2 = rotate_vector(t2, TAU / 4) + try: + return line_intersection(line1=(a1, a1 + n1), line2=(a2, a2 + n2)) + except Exception: + if warning: + warnings.warn("Can't find Arc center, using ORIGIN instead") + self._failed_to_get_center = True + return np.array(ORIGIN) + + def move_arc_center_to(self, point: Point3D) -> Self: + self.shift(point - self.get_arc_center()) + return self + + def stop_angle(self) -> float: + return angle_of_vector(self.points[-1] - self.get_arc_center()) % TAU + + +class ArcBetweenPoints(Arc): + """Inherits from Arc and additionally takes 2 points between which the arc is spanned. + + Example + ------- + .. manim:: ArcBetweenPointsExample + + class ArcBetweenPointsExample(Scene): + def construct(self): + circle = Circle(radius=2, stroke_color=GREY) + dot_1 = Dot(color=GREEN).move_to([2, 0, 0]).scale(0.5) + dot_1_text = Tex("(2,0)").scale(0.5).next_to(dot_1, RIGHT).set_color(BLUE) + dot_2 = Dot(color=GREEN).move_to([0, 2, 0]).scale(0.5) + dot_2_text = Tex("(0,2)").scale(0.5).next_to(dot_2, UP).set_color(BLUE) + arc= ArcBetweenPoints(start=2 * RIGHT, end=2 * UP, stroke_color=YELLOW) + self.add(circle, dot_1, dot_2, dot_1_text, dot_2_text) + self.play(Create(arc)) + """ + + def __init__( + self, + start: Point3D, + end: Point3D, + angle: float = TAU / 4, + radius: float = None, + **kwargs, + ) -> None: + if radius is not None: + self.radius = radius + if radius < 0: + sign = -2 + radius *= -1 + else: + sign = 2 + halfdist = np.linalg.norm(np.array(start) - np.array(end)) / 2 + if radius < halfdist: + raise ValueError( + """ArcBetweenPoints called with a radius that is + smaller than half the distance between the points.""", + ) + arc_height = radius - np.sqrt(radius**2 - halfdist**2) + angle = np.arccos((radius - arc_height) / radius) * sign + + super().__init__(radius=radius, angle=angle, **kwargs) + if angle == 0: + self.set_points_as_corners([LEFT, RIGHT]) + self.put_start_and_end_on(start, end) + + if radius is None: + center = self.get_arc_center(warning=False) + if not self._failed_to_get_center: + self.radius = np.linalg.norm(np.array(start) - np.array(center)) + else: + self.radius = np.inf + + +class CurvedArrow(ArcBetweenPoints): + def __init__(self, start_point: Point3D, end_point: Point3D, **kwargs) -> None: + from manim.mobject.geometry.tips import ArrowTriangleFilledTip + + tip_shape = kwargs.pop("tip_shape", ArrowTriangleFilledTip) + super().__init__(start_point, end_point, **kwargs) + self.add_tip(tip_shape=tip_shape) + + +class CurvedDoubleArrow(CurvedArrow): + def __init__(self, start_point: Point3D, end_point: Point3D, **kwargs) -> None: + if "tip_shape_end" in kwargs: + kwargs["tip_shape"] = kwargs.pop("tip_shape_end") + from manim.mobject.geometry.tips import ArrowTriangleFilledTip + + tip_shape_start = kwargs.pop("tip_shape_start", ArrowTriangleFilledTip) + super().__init__(start_point, end_point, **kwargs) + self.add_tip(at_start=True, tip_shape=tip_shape_start) + + +class Circle(Arc): + """A circle. + + Parameters + ---------- + color + The color of the shape. + kwargs + Additional arguments to be passed to :class:`Arc` + + Examples + -------- + .. manim:: CircleExample + :save_last_frame: + + class CircleExample(Scene): + def construct(self): + circle_1 = Circle(radius=1.0) + circle_2 = Circle(radius=1.5, color=GREEN) + circle_3 = Circle(radius=1.0, color=BLUE_B, fill_opacity=1) + + circle_group = Group(circle_1, circle_2, circle_3).arrange(buff=1) + self.add(circle_group) + """ + + def __init__( + self, + radius: float | None = None, + color: ParsableManimColor = RED, + **kwargs, + ) -> None: + super().__init__( + radius=radius, + start_angle=0, + angle=TAU, + color=color, + **kwargs, + ) + + def surround( + self, + mobject: Mobject, + dim_to_match: int = 0, + stretch: bool = False, + buffer_factor: float = 1.2, + ) -> Self: + """Modifies a circle so that it surrounds a given mobject. + + Parameters + ---------- + mobject + The mobject that the circle will be surrounding. + dim_to_match + buffer_factor + Scales the circle with respect to the mobject. A `buffer_factor` < 1 makes the circle smaller than the mobject. + stretch + Stretches the circle to fit more tightly around the mobject. Note: Does not work with :class:`Line` + + Examples + -------- + .. manim:: CircleSurround + :save_last_frame: + + class CircleSurround(Scene): + def construct(self): + triangle1 = Triangle() + circle1 = Circle().surround(triangle1) + group1 = Group(triangle1,circle1) # treat the two mobjects as one + + line2 = Line() + circle2 = Circle().surround(line2, buffer_factor=2.0) + group2 = Group(line2,circle2) + + # buffer_factor < 1, so the circle is smaller than the square + square3 = Square() + circle3 = Circle().surround(square3, buffer_factor=0.5) + group3 = Group(square3, circle3) + + group = Group(group1, group2, group3).arrange(buff=1) + self.add(group) + """ + + # Ignores dim_to_match and stretch; result will always be a circle + # TODO: Perhaps create an ellipse class to handle single-dimension stretching + + # Something goes wrong here when surrounding lines? + # TODO: Figure out and fix + self.replace(mobject, dim_to_match, stretch) + + self.width = np.sqrt(mobject.width**2 + mobject.height**2) + return self.scale(buffer_factor) + + def point_at_angle(self, angle: float) -> Point3D: + """Returns the position of a point on the circle. + + Parameters + ---------- + angle + The angle of the point along the circle in radians. + + Returns + ------- + :class:`numpy.ndarray` + The location of the point along the circle's circumference. + + Examples + -------- + .. manim:: PointAtAngleExample + :save_last_frame: + + class PointAtAngleExample(Scene): + def construct(self): + circle = Circle(radius=2.0) + p1 = circle.point_at_angle(PI/2) + p2 = circle.point_at_angle(270*DEGREES) + + s1 = Square(side_length=0.25).move_to(p1) + s2 = Square(side_length=0.25).move_to(p2) + self.add(circle, s1, s2) + + """ + + start_angle = angle_of_vector(self.points[0] - self.get_center()) + proportion = (angle - start_angle) / TAU + proportion -= np.floor(proportion) + return self.point_from_proportion(proportion) + + @staticmethod + def from_three_points(p1: Point3D, p2: Point3D, p3: Point3D, **kwargs) -> Self: + """Returns a circle passing through the specified + three points. + + Example + ------- + .. manim:: CircleFromPointsExample + :save_last_frame: + + class CircleFromPointsExample(Scene): + def construct(self): + circle = Circle.from_three_points(LEFT, LEFT + UP, UP * 2, color=RED) + dots = VGroup( + Dot(LEFT), + Dot(LEFT + UP), + Dot(UP * 2), + ) + self.add(NumberPlane(), circle, dots) + """ + center = line_intersection( + perpendicular_bisector([p1, p2]), + perpendicular_bisector([p2, p3]), + ) + radius = np.linalg.norm(p1 - center) + return Circle(radius=radius, **kwargs).shift(center) + + +class Dot(Circle): + """A circle with a very small radius. + + Parameters + ---------- + point + The location of the dot. + radius + The radius of the dot. + stroke_width + The thickness of the outline of the dot. + fill_opacity + The opacity of the dot's fill_colour + color + The color of the dot. + kwargs + Additional arguments to be passed to :class:`Circle` + + Examples + -------- + .. manim:: DotExample + :save_last_frame: + + class DotExample(Scene): + def construct(self): + dot1 = Dot(point=LEFT, radius=0.08) + dot2 = Dot(point=ORIGIN) + dot3 = Dot(point=RIGHT) + self.add(dot1,dot2,dot3) + """ + + def __init__( + self, + point: Point3D = ORIGIN, + radius: float = DEFAULT_DOT_RADIUS, + stroke_width: float = 0, + fill_opacity: float = 1.0, + color: ParsableManimColor = WHITE, + **kwargs, + ) -> None: + super().__init__( + arc_center=point, + radius=radius, + stroke_width=stroke_width, + fill_opacity=fill_opacity, + color=color, + **kwargs, + ) + + +class AnnotationDot(Dot): + """A dot with bigger radius and bold stroke to annotate scenes.""" + + def __init__( + self, + radius: float = DEFAULT_DOT_RADIUS * 1.3, + stroke_width: float = 5, + stroke_color: ParsableManimColor = WHITE, + fill_color: ParsableManimColor = BLUE, + **kwargs, + ) -> None: + super().__init__( + radius=radius, + stroke_width=stroke_width, + stroke_color=stroke_color, + fill_color=fill_color, + **kwargs, + ) + + +class LabeledDot(Dot): + """A :class:`Dot` containing a label in its center. + + Parameters + ---------- + label + The label of the :class:`Dot`. This is rendered as :class:`~.MathTex` + by default (i.e., when passing a :class:`str`), but other classes + representing rendered strings like :class:`~.Text` or :class:`~.Tex` + can be passed as well. + radius + The radius of the :class:`Dot`. If ``None`` (the default), the radius + is calculated based on the size of the ``label``. + + Examples + -------- + .. manim:: SeveralLabeledDots + :save_last_frame: + + class SeveralLabeledDots(Scene): + def construct(self): + sq = Square(fill_color=RED, fill_opacity=1) + self.add(sq) + dot1 = LabeledDot(Tex("42", color=RED)) + dot2 = LabeledDot(MathTex("a", color=GREEN)) + dot3 = LabeledDot(Text("ii", color=BLUE)) + dot4 = LabeledDot("3") + dot1.next_to(sq, UL) + dot2.next_to(sq, UR) + dot3.next_to(sq, DL) + dot4.next_to(sq, DR) + self.add(dot1, dot2, dot3, dot4) + """ + + def __init__( + self, + label: str | SingleStringMathTex | Text | Tex, + radius: float | None = None, + **kwargs, + ) -> None: + if isinstance(label, str): + from manim import MathTex + + rendered_label = MathTex(label, color=BLACK) + else: + rendered_label = label + + if radius is None: + radius = 0.1 + max(rendered_label.width, rendered_label.height) / 2 + super().__init__(radius=radius, **kwargs) + rendered_label.move_to(self.get_center()) + self.add(rendered_label) + + +class Ellipse(Circle): + """A circular shape; oval, circle. + + Parameters + ---------- + width + The horizontal width of the ellipse. + height + The vertical height of the ellipse. + kwargs + Additional arguments to be passed to :class:`Circle`. + + Examples + -------- + .. manim:: EllipseExample + :save_last_frame: + + class EllipseExample(Scene): + def construct(self): + ellipse_1 = Ellipse(width=2.0, height=4.0, color=BLUE_B) + ellipse_2 = Ellipse(width=4.0, height=1.0, color=BLUE_D) + ellipse_group = Group(ellipse_1,ellipse_2).arrange(buff=1) + self.add(ellipse_group) + """ + + def __init__(self, width: float = 2, height: float = 1, **kwargs) -> None: + super().__init__(**kwargs) + self.stretch_to_fit_width(width) + self.stretch_to_fit_height(height) + + +class AnnularSector(Arc): + """A sector of an annulus. + + + Parameters + ---------- + inner_radius + The inside radius of the Annular Sector. + outer_radius + The outside radius of the Annular Sector. + angle + The clockwise angle of the Annular Sector. + start_angle + The starting clockwise angle of the Annular Sector. + fill_opacity + The opacity of the color filled in the Annular Sector. + stroke_width + The stroke width of the Annular Sector. + color + The color filled into the Annular Sector. + + Examples + -------- + .. manim:: AnnularSectorExample + :save_last_frame: + + class AnnularSectorExample(Scene): + def construct(self): + # Changes background color to clearly visualize changes in fill_opacity. + self.camera.background_color = WHITE + + # The default parameter start_angle is 0, so the AnnularSector starts from the +x-axis. + s1 = AnnularSector(color=YELLOW).move_to(2 * UL) + + # Different inner_radius and outer_radius than the default. + s2 = AnnularSector(inner_radius=1.5, outer_radius=2, angle=45 * DEGREES, color=RED).move_to(2 * UR) + + # fill_opacity is typically a number > 0 and <= 1. If fill_opacity=0, the AnnularSector is transparent. + s3 = AnnularSector(inner_radius=1, outer_radius=1.5, angle=PI, fill_opacity=0.25, color=BLUE).move_to(2 * DL) + + # With a negative value for the angle, the AnnularSector is drawn clockwise from the start value. + s4 = AnnularSector(inner_radius=1, outer_radius=1.5, angle=-3 * PI / 2, color=GREEN).move_to(2 * DR) + + self.add(s1, s2, s3, s4) + """ + + def __init__( + self, + inner_radius: float = 1, + outer_radius: float = 2, + angle: float = TAU / 4, + start_angle: float = 0, + fill_opacity: float = 1, + stroke_width: float = 0, + color: ParsableManimColor = WHITE, + **kwargs, + ) -> None: + self.inner_radius = inner_radius + self.outer_radius = outer_radius + super().__init__( + start_angle=start_angle, + angle=angle, + fill_opacity=fill_opacity, + stroke_width=stroke_width, + color=color, + **kwargs, + ) + + def generate_points(self) -> None: + inner_arc, outer_arc = ( + Arc( + start_angle=self.start_angle, + angle=self.angle, + radius=radius, + arc_center=self.arc_center, + ) + for radius in (self.inner_radius, self.outer_radius) + ) + outer_arc.reverse_points() + self.append_points(inner_arc.points) + self.add_line_to(outer_arc.points[0]) + self.append_points(outer_arc.points) + self.add_line_to(inner_arc.points[0]) + + init_points = generate_points + + +class Sector(AnnularSector): + """A sector of a circle. + + Examples + -------- + .. manim:: ExampleSector + :save_last_frame: + + class ExampleSector(Scene): + def construct(self): + sector = Sector(outer_radius=2, inner_radius=1) + sector2 = Sector(outer_radius=2.5, inner_radius=0.8).move_to([-3, 0, 0]) + sector.set_color(RED) + sector2.set_color(PINK) + self.add(sector, sector2) + """ + + def __init__( + self, outer_radius: float = 1, inner_radius: float = 0, **kwargs + ) -> None: + super().__init__(inner_radius=inner_radius, outer_radius=outer_radius, **kwargs) + + +class Annulus(Circle): + """Region between two concentric :class:`Circles <.Circle>`. + + Parameters + ---------- + inner_radius + The radius of the inner :class:`Circle`. + outer_radius + The radius of the outer :class:`Circle`. + kwargs + Additional arguments to be passed to :class:`Annulus` + + Examples + -------- + .. manim:: AnnulusExample + :save_last_frame: + + class AnnulusExample(Scene): + def construct(self): + annulus_1 = Annulus(inner_radius=0.5, outer_radius=1).shift(UP) + annulus_2 = Annulus(inner_radius=0.3, outer_radius=0.6, color=RED).next_to(annulus_1, DOWN) + self.add(annulus_1, annulus_2) + """ + + def __init__( + self, + inner_radius: float | None = 1, + outer_radius: float | None = 2, + fill_opacity: float = 1, + stroke_width: float = 0, + color: ParsableManimColor = WHITE, + mark_paths_closed: bool = False, + **kwargs, + ) -> None: + self.mark_paths_closed = mark_paths_closed # is this even used? + self.inner_radius = inner_radius + self.outer_radius = outer_radius + super().__init__( + fill_opacity=fill_opacity, stroke_width=stroke_width, color=color, **kwargs + ) + + def generate_points(self) -> None: + self.radius = self.outer_radius + outer_circle = Circle(radius=self.outer_radius) + inner_circle = Circle(radius=self.inner_radius) + inner_circle.reverse_points() + self.append_points(outer_circle.points) + self.append_points(inner_circle.points) + self.shift(self.arc_center) + + init_points = generate_points + + +class CubicBezier(VMobject, metaclass=ConvertToOpenGL): + """A cubic Bézier curve. + + Example + ------- + .. manim:: BezierSplineExample + :save_last_frame: + + class BezierSplineExample(Scene): + def construct(self): + p1 = np.array([-3, 1, 0]) + p1b = p1 + [1, 0, 0] + d1 = Dot(point=p1).set_color(BLUE) + l1 = Line(p1, p1b) + p2 = np.array([3, -1, 0]) + p2b = p2 - [1, 0, 0] + d2 = Dot(point=p2).set_color(RED) + l2 = Line(p2, p2b) + bezier = CubicBezier(p1b, p1b + 3 * RIGHT, p2b - 3 * RIGHT, p2b) + self.add(l1, d1, l2, d2, bezier) + + """ + + def __init__( + self, + start_anchor: CubicBezierPoints, + start_handle: CubicBezierPoints, + end_handle: CubicBezierPoints, + end_anchor: CubicBezierPoints, + **kwargs, + ) -> None: + super().__init__(**kwargs) + self.add_cubic_bezier_curve(start_anchor, start_handle, end_handle, end_anchor) + + +class ArcPolygon(VMobject, metaclass=ConvertToOpenGL): + """A generalized polygon allowing for points to be connected with arcs. + + This version tries to stick close to the way :class:`Polygon` is used. Points + can be passed to it directly which are used to generate the according arcs + (using :class:`ArcBetweenPoints`). An angle or radius can be passed to it to + use across all arcs, but to configure arcs individually an ``arc_config`` list + has to be passed with the syntax explained below. + + Parameters + ---------- + vertices + A list of vertices, start and end points for the arc segments. + angle + The angle used for constructing the arcs. If no other parameters + are set, this angle is used to construct all arcs. + radius + The circle radius used to construct the arcs. If specified, + overrides the specified ``angle``. + arc_config + When passing a ``dict``, its content will be passed as keyword + arguments to :class:`~.ArcBetweenPoints`. Otherwise, a list + of dictionaries containing values that are passed as keyword + arguments for every individual arc can be passed. + kwargs + Further keyword arguments that are passed to the constructor of + :class:`~.VMobject`. + + Attributes + ---------- + arcs : :class:`list` + The arcs created from the input parameters:: + + >>> from manim import ArcPolygon + >>> ap = ArcPolygon([0, 0, 0], [2, 0, 0], [0, 2, 0]) + >>> ap.arcs + [ArcBetweenPoints, ArcBetweenPoints, ArcBetweenPoints] + + + .. tip:: + + Two instances of :class:`ArcPolygon` can be transformed properly into one + another as well. Be advised that any arc initialized with ``angle=0`` + will actually be a straight line, so if a straight section should seamlessly + transform into an arced section or vice versa, initialize the straight section + with a negligible angle instead (such as ``angle=0.0001``). + + .. note:: + There is an alternative version (:class:`ArcPolygonFromArcs`) that is instantiated + with pre-defined arcs. + + See Also + -------- + :class:`ArcPolygonFromArcs` + + + Examples + -------- + .. manim:: SeveralArcPolygons + + class SeveralArcPolygons(Scene): + def construct(self): + a = [0, 0, 0] + b = [2, 0, 0] + c = [0, 2, 0] + ap1 = ArcPolygon(a, b, c, radius=2) + ap2 = ArcPolygon(a, b, c, angle=45*DEGREES) + ap3 = ArcPolygon(a, b, c, arc_config={'radius': 1.7, 'color': RED}) + ap4 = ArcPolygon(a, b, c, color=RED, fill_opacity=1, + arc_config=[{'radius': 1.7, 'color': RED}, + {'angle': 20*DEGREES, 'color': BLUE}, + {'radius': 1}]) + ap_group = VGroup(ap1, ap2, ap3, ap4).arrange() + self.play(*[Create(ap) for ap in [ap1, ap2, ap3, ap4]]) + self.wait() + + For further examples see :class:`ArcPolygonFromArcs`. + """ + + def __init__( + self, + *vertices: Point3D, + angle: float = PI / 4, + radius: float | None = None, + arc_config: list[dict] | None = None, + **kwargs, + ) -> None: + n = len(vertices) + point_pairs = [(vertices[k], vertices[(k + 1) % n]) for k in range(n)] + + if not arc_config: + if radius: + all_arc_configs = itertools.repeat({"radius": radius}, len(point_pairs)) + else: + all_arc_configs = itertools.repeat({"angle": angle}, len(point_pairs)) + elif isinstance(arc_config, dict): + all_arc_configs = itertools.repeat(arc_config, len(point_pairs)) + else: + assert len(arc_config) == n + all_arc_configs = arc_config + + arcs = [ + ArcBetweenPoints(*pair, **conf) + for (pair, conf) in zip(point_pairs, all_arc_configs) + ] + + super().__init__(**kwargs) + # Adding the arcs like this makes ArcPolygon double as a VGroup. + # Also makes changes to the ArcPolygon, such as scaling, affect + # the arcs, so that their new values are usable. + self.add(*arcs) + for arc in arcs: + self.append_points(arc.points) + + # This enables the use of ArcPolygon.arcs as a convenience + # because ArcPolygon[0] returns itself, not the first Arc. + self.arcs = arcs + + +class ArcPolygonFromArcs(VMobject, metaclass=ConvertToOpenGL): + """A generalized polygon allowing for points to be connected with arcs. + + This version takes in pre-defined arcs to generate the arcpolygon and introduces + little new syntax. However unlike :class:`Polygon` it can't be created with points + directly. + + For proper appearance the passed arcs should connect seamlessly: + ``[a,b][b,c][c,a]`` + + If there are any gaps between the arcs, those will be filled in + with straight lines, which can be used deliberately for any straight + sections. Arcs can also be passed as straight lines such as an arc + initialized with ``angle=0``. + + Parameters + ---------- + arcs + These are the arcs from which the arcpolygon is assembled. + kwargs + Keyword arguments that are passed to the constructor of + :class:`~.VMobject`. Affects how the ArcPolygon itself is drawn, + but doesn't affect passed arcs. + + Attributes + ---------- + arcs + The arcs used to initialize the ArcPolygonFromArcs:: + + >>> from manim import ArcPolygonFromArcs, Arc, ArcBetweenPoints + >>> ap = ArcPolygonFromArcs(Arc(), ArcBetweenPoints([1,0,0], [0,1,0]), Arc()) + >>> ap.arcs + [Arc, ArcBetweenPoints, Arc] + + + .. tip:: + + Two instances of :class:`ArcPolygon` can be transformed properly into + one another as well. Be advised that any arc initialized with ``angle=0`` + will actually be a straight line, so if a straight section should seamlessly + transform into an arced section or vice versa, initialize the straight + section with a negligible angle instead (such as ``angle=0.0001``). + + .. note:: + There is an alternative version (:class:`ArcPolygon`) that can be instantiated + with points. + + .. seealso:: + :class:`ArcPolygon` + + Examples + -------- + One example of an arcpolygon is the Reuleaux triangle. + Instead of 3 straight lines connecting the outer points, + a Reuleaux triangle has 3 arcs connecting those points, + making a shape with constant width. + + Passed arcs are stored as submobjects in the arcpolygon. + This means that the arcs are changed along with the arcpolygon, + for example when it's shifted, and these arcs can be manipulated + after the arcpolygon has been initialized. + + Also both the arcs contained in an :class:`~.ArcPolygonFromArcs`, as well as the + arcpolygon itself are drawn, which affects draw time in :class:`~.Create` + for example. In most cases the arcs themselves don't + need to be drawn, in which case they can be passed as invisible. + + .. manim:: ArcPolygonExample + + class ArcPolygonExample(Scene): + def construct(self): + arc_conf = {"stroke_width": 0} + poly_conf = {"stroke_width": 10, "stroke_color": BLUE, + "fill_opacity": 1, "color": PURPLE} + a = [-1, 0, 0] + b = [1, 0, 0] + c = [0, np.sqrt(3), 0] + arc0 = ArcBetweenPoints(a, b, radius=2, **arc_conf) + arc1 = ArcBetweenPoints(b, c, radius=2, **arc_conf) + arc2 = ArcBetweenPoints(c, a, radius=2, **arc_conf) + reuleaux_tri = ArcPolygonFromArcs(arc0, arc1, arc2, **poly_conf) + self.play(FadeIn(reuleaux_tri)) + self.wait(2) + + The arcpolygon itself can also be hidden so that instead only the contained + arcs are drawn. This can be used to easily debug arcs or to highlight them. + + .. manim:: ArcPolygonExample2 + + class ArcPolygonExample2(Scene): + def construct(self): + arc_conf = {"stroke_width": 3, "stroke_color": BLUE, + "fill_opacity": 0.5, "color": GREEN} + poly_conf = {"color": None} + a = [-1, 0, 0] + b = [1, 0, 0] + c = [0, np.sqrt(3), 0] + arc0 = ArcBetweenPoints(a, b, radius=2, **arc_conf) + arc1 = ArcBetweenPoints(b, c, radius=2, **arc_conf) + arc2 = ArcBetweenPoints(c, a, radius=2, stroke_color=RED) + reuleaux_tri = ArcPolygonFromArcs(arc0, arc1, arc2, **poly_conf) + self.play(FadeIn(reuleaux_tri)) + self.wait(2) + """ + + def __init__(self, *arcs: Arc | ArcBetweenPoints, **kwargs) -> None: + if not all(isinstance(m, (Arc, ArcBetweenPoints)) for m in arcs): + raise ValueError( + "All ArcPolygon submobjects must be of type Arc/ArcBetweenPoints", + ) + super().__init__(**kwargs) + # Adding the arcs like this makes ArcPolygonFromArcs double as a VGroup. + # Also makes changes to the ArcPolygonFromArcs, such as scaling, affect + # the arcs, so that their new values are usable. + self.add(*arcs) + # This enables the use of ArcPolygonFromArcs.arcs as a convenience + # because ArcPolygonFromArcs[0] returns itself, not the first Arc. + self.arcs = [*arcs] + from .line import Line + + for arc1, arc2 in adjacent_pairs(arcs): + self.append_points(arc1.points) + line = Line(arc1.get_end(), arc2.get_start()) + len_ratio = line.get_length() / arc1.get_arc_length() + if np.isnan(len_ratio) or np.isinf(len_ratio): + continue + line.insert_n_curves(int(arc1.get_num_curves() * len_ratio)) + self.append_points(line.points)