From 7f940fbee4729d50f8a8656759105c88d551b34d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 17:45:52 -0800 Subject: [PATCH 01/24] Change how ShaderWrapper uniforms are handled --- manimlib/mobject/mobject.py | 6 +-- manimlib/mobject/types/vectorized_mobject.py | 50 +++++++++++--------- manimlib/scene/scene.py | 5 +- manimlib/shader_wrapper.py | 28 ++++++----- 4 files changed, 49 insertions(+), 40 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 34052b16..cf35b3b3 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -1967,7 +1967,7 @@ class Mobject(object): self.shader_wrapper.vert_data = self.get_shader_data() self.shader_wrapper.vert_indices = self.get_shader_vert_indices() - self.shader_wrapper.update_program_uniforms(self.get_uniforms()) + self.shader_wrapper.bind_to_mobject_uniforms(self.get_uniforms()) self.shader_wrapper.depth_test = self.depth_test return self.shader_wrapper @@ -2004,9 +2004,7 @@ class Mobject(object): shader_wrapper.generate_vao() self._data_has_changed = False for shader_wrapper in self.shader_wrappers: - shader_wrapper.depth_test = self.depth_test - shader_wrapper.update_program_uniforms(self.get_uniforms()) - shader_wrapper.update_program_uniforms(camera_uniforms, universal=True) + shader_wrapper.update_program_uniforms(camera_uniforms) shader_wrapper.pre_render() shader_wrapper.render() diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 461d0fe6..59bf2393 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1284,14 +1284,14 @@ class VMobject(Mobject): self.fill_shader_wrapper = FillShaderWrapper( ctx=ctx, vert_data=fill_data, - uniforms=self.uniforms, + mobject_uniforms=self.uniforms, shader_folder=self.fill_shader_folder, render_primitive=self.fill_render_primitive, ) self.stroke_shader_wrapper = ShaderWrapper( ctx=ctx, vert_data=stroke_data, - uniforms=self.uniforms, + mobject_uniforms=self.uniforms, shader_folder=self.stroke_shader_folder, render_primitive=self.stroke_render_primitive, ) @@ -1309,11 +1309,6 @@ class VMobject(Mobject): wrapper.refresh_id() return self - def get_uniforms(self): - # TODO, account for submob uniforms separately? - self.uniforms.update(self.family_members_with_points()[0].uniforms) - return self.uniforms - def get_shader_wrapper_list(self, ctx: Context) -> list[ShaderWrapper]: if not self._shaders_initialized: self.init_shader_data(ctx) @@ -1325,32 +1320,25 @@ class VMobject(Mobject): fill_names = self.fill_data_names stroke_names = self.stroke_data_names - # Build up data lists + fill_family = (sm for sm in family if sm._has_fill) + stroke_family = (sm for sm in family if sm._has_stroke) + + # Build up fill data lists fill_datas = [] fill_indices = [] fill_border_datas = [] - stroke_datas = [] - back_stroke_datas = [] - for submob in family: - submob.get_joint_products() + for submob in fill_family: indices = submob.get_outer_vert_indices() - has_fill = submob._has_fill - has_stroke = submob._has_stroke - back_stroke = has_stroke and submob.stroke_behind - front_stroke = has_stroke and not submob.stroke_behind - if back_stroke: - back_stroke_datas.append(submob.data[stroke_names][indices]) - if front_stroke: - stroke_datas.append(submob.data[stroke_names][indices]) - if has_fill and submob._use_winding_fill: + if submob._use_winding_fill: data = submob.data[fill_names] data["base_point"][:] = data["point"][0] fill_datas.append(data[indices]) - if has_fill and not submob._use_winding_fill: + else: fill_datas.append(submob.data[fill_names]) fill_indices.append(submob.get_triangulation()) - if has_fill and not front_stroke: + if (not submob._has_stroke) or submob.stroke_behind: # Add fill border + submob.get_joint_products() names = list(stroke_names) names[names.index('stroke_rgba')] = 'fill_rgba' names[names.index('stroke_width')] = 'fill_border_width' @@ -1359,11 +1347,25 @@ class VMobject(Mobject): ) fill_border_datas.append(border_stroke_data[indices]) + # Build up stroke data lists + stroke_datas = [] + back_stroke_datas = [] + for submob in stroke_family: + submob.get_joint_products() + indices = submob.get_outer_vert_indices() + if submob.stroke_behind: + back_stroke_datas.append(submob.data[stroke_names][indices]) + else: + stroke_datas.append(submob.data[stroke_names][indices]) + shader_wrappers = [ self.back_stroke_shader_wrapper.read_in([*back_stroke_datas, *fill_border_datas]), self.fill_shader_wrapper.read_in(fill_datas, fill_indices or None), self.stroke_shader_wrapper.read_in(stroke_datas), ] + for sw in shader_wrappers: + sw.bind_to_mobject_uniforms(family[0].get_uniforms()) + sw.depth_test = family[0].depth_test return [sw for sw in shader_wrappers if len(sw.vert_data) > 0] @@ -1371,6 +1373,8 @@ class VGroup(VMobject): def __init__(self, *vmobjects: VMobject, **kwargs): super().__init__(**kwargs) self.add(*vmobjects) + if vmobjects: + self.uniforms.update(vmobjects[0].uniforms) def __add__(self, other: VMobject) -> Self: assert(isinstance(other, VMobject)) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 0a764737..d2af205a 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -387,7 +387,10 @@ class Scene(object): same type are grouped together, so this function creates Groups of all clusters of adjacent Mobjects in the scene """ - batches = batch_by_property(self.mobjects, lambda m: str(type(m))) + batches = batch_by_property( + self.mobjects, + lambda m: str(type(m)) + str(m.get_uniforms()) + ) for group in self.render_groups: group.clear() diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index dc5de477..454d78fe 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -37,7 +37,7 @@ class ShaderWrapper(object): vert_data: np.ndarray, vert_indices: Optional[np.ndarray] = None, shader_folder: Optional[str] = None, - uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables + mobject_uniforms: Optional[UniformDict] = None, # A dictionary mapping names of uniform variables texture_paths: Optional[dict[str, str]] = None, # A dictionary mapping names to filepaths for textures. depth_test: bool = False, render_primitive: int = moderngl.TRIANGLE_STRIP, @@ -47,13 +47,14 @@ class ShaderWrapper(object): self.vert_indices = (vert_indices or np.zeros(0)).astype(int) self.vert_attributes = vert_data.dtype.names self.shader_folder = shader_folder - self.uniforms: UniformDict = dict() self.depth_test = depth_test self.render_primitive = render_primitive + self.program_uniform_mirror: UniformDict = dict() + self.bind_to_mobject_uniforms(mobject_uniforms) + self.init_program_code() self.init_program() - self.update_program_uniforms(uniforms or dict()) if texture_paths is not None: self.init_textures(texture_paths) self.init_vao() @@ -91,14 +92,17 @@ class ShaderWrapper(object): self.ibo = None self.vao = None + def bind_to_mobject_uniforms(self, mobject_uniforms: UniformDict): + self.mobject_uniforms = mobject_uniforms + def __eq__(self, shader_wrapper: ShaderWrapper): return all(( np.all(self.vert_data == shader_wrapper.vert_data), np.all(self.vert_indices == shader_wrapper.vert_indices), self.shader_folder == shader_wrapper.shader_folder, all( - self.uniforms[key] == shader_wrapper.uniforms[key] - for key in self.uniforms + self.mobject_uniforms[key] == shader_wrapper.mobject_uniforms[key] + for key in self.mobject_uniforms ), self.depth_test == shader_wrapper.depth_test, self.render_primitive == shader_wrapper.render_primitive, @@ -129,7 +133,7 @@ class ShaderWrapper(object): # A unique id for a shader return "|".join(map(str, [ self.program_id, - self.uniforms, + self.mobject_uniforms, self.depth_test, self.render_primitive, ])) @@ -155,9 +159,9 @@ class ShaderWrapper(object): # Changing context def use_clip_plane(self): - if "clip_plane" not in self.uniforms: + if "clip_plane" not in self.mobject_uniforms: return False - return any(self.uniforms["clip_plane"]) + return any(self.mobject_uniforms["clip_plane"]) def set_ctx_depth_test(self, enable: bool = True) -> None: if enable: @@ -222,18 +226,18 @@ class ShaderWrapper(object): assert(self.vao is not None) self.vao.render() - def update_program_uniforms(self, uniforms: UniformDict, universal: bool = False): + def update_program_uniforms(self, camera_uniforms: UniformDict): if self.program is None: return - for name, value in uniforms.items(): + for name, value in (*self.mobject_uniforms.items(), *camera_uniforms.items()): if name not in self.program: continue if isinstance(value, np.ndarray) and value.ndim > 0: value = tuple(value) - if universal and self.uniforms.get(name, None) == value: + if name in camera_uniforms and self.program_uniform_mirror.get(name, None) == value: continue self.program[name].value = value - self.uniforms[name] = value + self.program_uniform_mirror[name] = value def get_vertex_buffer_object(self, refresh: bool = True): if refresh: From 009f9dd18b6cf2ae408e27adf56e74de6162eabc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 18:16:44 -0800 Subject: [PATCH 02/24] Don't call become at the end of Transform --- manimlib/animation/transform.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 8192a36e..fdc262a9 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -74,8 +74,6 @@ class Transform(Animation): def finish(self) -> None: super().finish() self.mobject.unlock_data() - if self.target_mobject is not None and self.rate_func(1) == 1: - self.mobject.become(self.target_mobject) def create_target(self) -> Mobject: # Has no meaningful effect here, but may be useful From 4629e08769a79cef9d603485059d7c1eb30f581e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 18:17:12 -0800 Subject: [PATCH 03/24] Ensure joint_products are computed at both the start and end of an animation --- manimlib/mobject/types/vectorized_mobject.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 59bf2393..8b203f0e 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1262,11 +1262,10 @@ class VMobject(Mobject): def set_animating_status(self, is_animating: bool, recurse: bool = True): super().set_animating_status(is_animating, recurse) - if is_animating: - for submob in self.get_family(recurse): - submob.get_joint_products(refresh=True) - if not submob._use_winding_fill: - submob.get_triangulation() + for submob in self.get_family(recurse): + submob.get_joint_products(refresh=True) + if not submob._use_winding_fill: + submob.get_triangulation() return self # For shaders From 3d0fe27c553f566fe58b5c9daf4e81483e01f9ab Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 18:23:41 -0800 Subject: [PATCH 04/24] Simplify VMobject.set_rgba_array --- manimlib/mobject/types/vectorized_mobject.py | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 8b203f0e..ce4eae34 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -166,20 +166,12 @@ class VMobject(Mobject): def set_rgba_array( self, rgba_array: Vect4Array, - name: str | None = None, + name: str = "stroke_rgba", recurse: bool = False ) -> Self: - if name is None: - names = ["fill_rgba", "stroke_rgba"] - else: - names = [name] - - for name in names: - super().set_rgba_array(rgba_array, name, recurse) - if name == "fill_rgba": - self.note_changed_fill() - elif name == "stroke_rgba": - self.note_changed_stroke() + super().set_rgba_array(rgba_array, name, recurse) + self.note_changed_fill() + self.note_changed_stroke() return self def set_fill( From 88959df7a8eda53c36f3c815bb3513c9d0eada46 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 18:24:12 -0800 Subject: [PATCH 05/24] Use set_color instead of set_rgba_array in vector_field --- manimlib/mobject/vector_field.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/vector_field.py b/manimlib/mobject/vector_field.py index 02463de2..38ca9dc5 100644 --- a/manimlib/mobject/vector_field.py +++ b/manimlib/mobject/vector_field.py @@ -13,6 +13,7 @@ from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.utils.bezier import interpolate from manimlib.utils.bezier import inverse_interpolate from manimlib.utils.color import get_colormap_list +from manimlib.utils.color import rgb_to_color from manimlib.utils.dict_ops import merge_dicts_recursively from manimlib.utils.rate_functions import linear from manimlib.utils.simple_functions import sigmoid @@ -173,7 +174,10 @@ class VectorField(VGroup): **vector_config ) vect.shift(_input - origin) - vect.set_rgba_array([[*self.value_to_rgb(norm), self.opacity]]) + vect.set_color( + rgb_to_color(self.value_to_rgb(norm)), + opacity=self.opacity, + ) return vect From d10745a3791972341e970ab09328ba84a116309b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 20:47:12 -0800 Subject: [PATCH 06/24] Have CameraFrame.get_view_matrix and and CameraFrame.get_implied_camera_location use _data_has_changed instead of a refresh arg --- manimlib/camera/camera_frame.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/manimlib/camera/camera_frame.py b/manimlib/camera/camera_frame.py index deafefe3..e8077d7b 100644 --- a/manimlib/camera/camera_frame.py +++ b/manimlib/camera/camera_frame.py @@ -41,11 +41,6 @@ class CameraFrame(Mobject): self.set_height(frame_shape[1], stretch=True) self.move_to(center_point) - def note_changed_data(self, recurse_up: bool = True): - super().note_changed_data(recurse_up) - self.get_view_matrix(refresh=True) - self.get_implied_camera_location(refresh=True) - def set_orientation(self, rotation: Rotation): self.uniforms["orientation"][:] = rotation.as_quat() return self @@ -89,7 +84,7 @@ class CameraFrame(Mobject): Returns a 4x4 for the affine transformation mapping a point into the camera's internal coordinate system """ - if refresh: + if self._data_has_changed: shift = np.identity(4) rotation = np.identity(4) scale_mat = np.identity(4) @@ -169,10 +164,12 @@ class CameraFrame(Mobject): self.rotate(dgamma, self.get_inverse_camera_rotation_matrix()[2]) return self + @Mobject.affects_data def set_focal_distance(self, focal_distance: float): 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): self.uniforms["fovy"] = field_of_view return self @@ -202,8 +199,8 @@ class CameraFrame(Mobject): def get_field_of_view(self) -> float: return self.uniforms["fovy"] - def get_implied_camera_location(self, refresh=False) -> np.ndarray: - if refresh: + def get_implied_camera_location(self) -> np.ndarray: + if self._data_has_changed: to_camera = self.get_inverse_camera_rotation_matrix()[2] dist = self.get_focal_distance() self.camera_location = self.get_center() + dist * to_camera From c4777015fca6a22d34f3f79f254170748e5774b3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 20:47:55 -0800 Subject: [PATCH 07/24] FIx Mobject.replace_shader_code --- manimlib/mobject/mobject.py | 13 ++++++++----- manimlib/mobject/types/vectorized_mobject.py | 9 +++++++-- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index cf35b3b3..bad88b34 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -105,6 +105,7 @@ class Mobject(object): 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() self._data_defaults = np.ones(1, dtype=self.data.dtype) @@ -1895,12 +1896,12 @@ class Mobject(object): # Shader code manipulation + @affects_data def replace_shader_code(self, old: str, new: str) -> Self: - # TODO, will this work with VMobject structure, given - # that it does not simpler return shader_wrappers of - # family? - for wrapper in self.get_shader_wrapper_list(): - wrapper.replace_code(old, new) + self.shader_code_replacements[old] = new + self._shaders_initialized = False + for mob in self.get_ancestors(): + mob._shaders_initialized = False return self def set_color_by_code(self, glsl_code: str) -> Self: @@ -1969,6 +1970,8 @@ class Mobject(object): self.shader_wrapper.vert_indices = self.get_shader_vert_indices() self.shader_wrapper.bind_to_mobject_uniforms(self.get_uniforms()) self.shader_wrapper.depth_test = self.depth_test + for old, new in self.shader_code_replacements.items(): + self.shader_wrapper.replace_code(old, new) return self.shader_wrapper def get_shader_wrapper_list(self, ctx: Context) -> list[ShaderWrapper]: diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index ce4eae34..9b27ddb9 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1292,6 +1292,10 @@ class VMobject(Mobject): self.fill_shader_wrapper, self.stroke_shader_wrapper, ] + for sw in self.shader_wrappers: + rep = self.family_members_with_points()[0] + for old, new in rep.shader_code_replacements.items(): + sw.replace_code(old, new) def refresh_shader_wrapper_id(self) -> Self: if not self._shaders_initialized: @@ -1355,8 +1359,9 @@ class VMobject(Mobject): self.stroke_shader_wrapper.read_in(stroke_datas), ] for sw in shader_wrappers: - sw.bind_to_mobject_uniforms(family[0].get_uniforms()) - sw.depth_test = family[0].depth_test + rep = family[0] # Representative family member + sw.bind_to_mobject_uniforms(rep.get_uniforms()) + sw.depth_test = rep.depth_test return [sw for sw in shader_wrappers if len(sw.vert_data) > 0] From ee08c552bfdc3fd5da9a3bcc121c7b15cf245e9c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 20:49:13 -0800 Subject: [PATCH 08/24] Remove ShaderWrapper.get_program_id --- manimlib/shader_wrapper.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 454d78fe..f8091271 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -126,31 +126,25 @@ class ShaderWrapper(object): def get_id(self) -> str: return self.id - def get_program_id(self) -> int: - return self.program_id - def create_id(self) -> str: # A unique id for a shader + program_id = hash("".join( + self.program_code[f"{name}_shader"] or "" + for name in ("vertex", "geometry", "fragment") + )) return "|".join(map(str, [ - self.program_id, + program_id, self.mobject_uniforms, self.depth_test, self.render_primitive, ])) def refresh_id(self) -> None: - self.program_id = self.create_program_id() self.id = self.create_id() - def create_program_id(self) -> int: - return hash("".join(( - self.program_code[f"{name}_shader"] or "" - for name in ("vertex", "geometry", "fragment") - ))) - def replace_code(self, old: str, new: str) -> None: code_map = self.program_code - for (name, code) in code_map.items(): + for name in code_map: if code_map[name] is None: continue code_map[name] = re.sub(old, new, code_map[name]) From 772ea792d02f7f93188df6aabff99c9f1642a1dc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 21:12:42 -0800 Subject: [PATCH 09/24] Add check for null VMobject in shader init --- manimlib/mobject/types/vectorized_mobject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 9b27ddb9..8c43b80f 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1293,7 +1293,8 @@ class VMobject(Mobject): self.stroke_shader_wrapper, ] for sw in self.shader_wrappers: - rep = self.family_members_with_points()[0] + family = self.family_members_with_points() + rep = family[0] if family else self for old, new in rep.shader_code_replacements.items(): sw.replace_code(old, new) From ac3db9b636f7c33aecb955a6eeaece9f76ad2b63 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 2 Feb 2023 21:13:18 -0800 Subject: [PATCH 10/24] Add set_program_uniform function --- manimlib/shader_wrapper.py | 10 ++-------- manimlib/utils/shaders.py | 31 ++++++++++++++++++++++++++++++- 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index f8091271..d47cb92f 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -15,6 +15,7 @@ from manimlib.utils.shaders import image_path_to_texture from manimlib.utils.shaders import get_texture_id from manimlib.utils.shaders import get_fill_canvas from manimlib.utils.shaders import release_texture +from manimlib.utils.shaders import set_program_uniform from typing import TYPE_CHECKING @@ -224,14 +225,7 @@ class ShaderWrapper(object): if self.program is None: return for name, value in (*self.mobject_uniforms.items(), *camera_uniforms.items()): - if name not in self.program: - continue - if isinstance(value, np.ndarray) and value.ndim > 0: - value = tuple(value) - if name in camera_uniforms and self.program_uniform_mirror.get(name, None) == value: - continue - self.program[name].value = value - self.program_uniform_mirror[name] = value + set_program_uniform(self.program, name, value) def get_vertex_buffer_object(self, refresh: bool = True): if refresh: diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 2a24fd76..3e4f67cf 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -9,7 +9,6 @@ import numpy as np from manimlib.config import parse_cli from manimlib.config import get_configuration -from manimlib.utils.customization import get_customization from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file @@ -17,11 +16,13 @@ from typing import TYPE_CHECKING if TYPE_CHECKING: from typing import Sequence, Optional, Tuple + from manimlib.typing import UniformDict from moderngl.vertex_array import VertexArray from moderngl.framebuffer import Framebuffer ID_TO_TEXTURE: dict[int, moderngl.Texture] = dict() +PROGRAM_UNIFORM_MIRRORS: dict[int, dict[str, float | tuple]] = dict() @lru_cache() @@ -63,6 +64,34 @@ def get_shader_program( ) +def set_program_uniform( + program: moderngl.Program, + name: str, + value: float | tuple | np.ndarray +) -> bool: + """ + Sets a program uniform, and also keeps track of a dictionary + of previously set uniforms for that program so that it + doesn't needlessly reset it, requiring an exchange with gpu + memory, if it sees the same value again. + + Returns True if changed the program, False if it left it as is. + """ + if name not in program: + return False + pid = id(program) + if pid not in PROGRAM_UNIFORM_MIRRORS: + PROGRAM_UNIFORM_MIRRORS[pid] = dict() + uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid] + if isinstance(value, np.ndarray) and value.ndim > 0: + value = tuple(value) + if uniform_mirror.get(name, None) == value: + return False + uniform_mirror[name] = value + program[name].value = value + return True + + @lru_cache() def get_shader_code_from_file(filename: str) -> str | None: if not filename: From 7c561d3757bc9b4edde8474f3682ad91dc2a10a9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 11:05:40 -0800 Subject: [PATCH 11/24] Edit set_program_uniform --- manimlib/utils/shaders.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/manimlib/utils/shaders.py b/manimlib/utils/shaders.py index 3e4f67cf..7cc1ba5c 100644 --- a/manimlib/utils/shaders.py +++ b/manimlib/utils/shaders.py @@ -77,18 +77,22 @@ def set_program_uniform( Returns True if changed the program, False if it left it as is. """ - if name not in program: - return False + pid = id(program) if pid not in PROGRAM_UNIFORM_MIRRORS: PROGRAM_UNIFORM_MIRRORS[pid] = dict() uniform_mirror = PROGRAM_UNIFORM_MIRRORS[pid] - if isinstance(value, np.ndarray) and value.ndim > 0: + + if type(value) is np.ndarray and value.ndim > 0: value = tuple(value) if uniform_mirror.get(name, None) == value: return False + + try: + program[name].value = value + except KeyError: + return False uniform_mirror[name] = value - program[name].value = value return True From b25f022859e54de027839b511d2242bb3aa6cf8e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 11:06:07 -0800 Subject: [PATCH 12/24] Make it an option (default to false) to prerun a scene to calculate its number of frames --- manimlib/config.py | 7 +++++++ manimlib/extract_scene.py | 8 +++++--- manimlib/scene/scene_file_writer.py | 8 ++++---- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index e3e93420..e8a26809 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -160,6 +160,12 @@ def parse_cli(): action="store_true", help="Show progress bar for each animation", ) + parser.add_argument( + "--prerun", + action="store_true", + help="Calculate total framecount, to display in a progress bar, by doing " + \ + "an initial run of the scene which skips animations." + ) parser.add_argument( "--video_dir", help="Directory to write video", @@ -489,6 +495,7 @@ def get_configuration(args: Namespace) -> dict: "presenter_mode": args.presenter_mode, "leave_progress_bars": args.leave_progress_bars, "show_animation_progress": args.show_animation_progress, + "prerun": args.prerun, "embed_exception_mode": custom_config["embed_exception_mode"], "embed_error_sound": custom_config["embed_error_sound"], } diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 66c27a2b..8f1a36ab 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -61,13 +61,15 @@ def get_scene_config(config): } -def compute_total_frames(scene_class, scene_config): +def compute_total_frames(scene_class, scene_config, config): """ When a scene is being written to file, a copy of the scene is run with skip_animations set to true so as to count how many frames it will require. This allows for a total progress bar on rendering, and also allows runtime errors to be exposed preemptively for long running scenes. """ + if not config["prerun"]: + return -1 pre_config = copy.deepcopy(scene_config) pre_config["file_writer_config"]["write_to_movie"] = False pre_config["file_writer_config"]["save_last_frame"] = False @@ -90,7 +92,7 @@ def get_scenes_to_render(scene_classes, scene_config, config): if scene_class.__name__ == scene_name: fw_config = scene_config["file_writer_config"] if fw_config["write_to_movie"]: - fw_config["total_frames"] = compute_total_frames(scene_class, scene_config) + fw_config["total_frames"] = compute_total_frames(scene_class, scene_config, config) scene = scene_class(**scene_config) result.append(scene) found = True @@ -109,7 +111,7 @@ def get_scenes_to_render(scene_classes, scene_config, config): for scene_class in scene_classes: fw_config = scene_config["file_writer_config"] if fw_config["write_to_movie"]: - fw_config["total_frames"] = compute_total_frames(scene_class, scene_config) + fw_config["total_frames"] = compute_total_frames(scene_class, scene_config, config) scene = scene_class(**scene_config) result.append(scene) return result diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index d288e7dd..bd2e9b19 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -46,7 +46,7 @@ class SceneFileWriter(object): show_file_location_upon_completion: bool = False, quiet: bool = False, total_frames: int = 0, - progress_description_len: int = 40, + progress_description_len: int | None = None, ): self.scene: Scene = scene self.write_to_movie = write_to_movie @@ -62,7 +62,8 @@ class SceneFileWriter(object): self.show_file_location_upon_completion = show_file_location_upon_completion self.quiet = quiet self.total_frames = total_frames - self.progress_description_len = progress_description_len + self.progress_description_len = progress_description_len or \ + 40 if total_frames > 0 else 80 # State during file writing self.writing_process: sp.Popen | None = None @@ -278,10 +279,9 @@ class SceneFileWriter(object): command += [self.temp_file_path] self.writing_process = sp.Popen(command, stdin=sp.PIPE) - if self.total_frames > 0 and not self.quiet: + if not self.quiet: self.progress_display = ProgressDisplay( range(self.total_frames), - # bar_format="{l_bar}{bar}|{n_fmt}/{total_fmt}", leave=False, ascii=True if platform.system() == 'Windows' else None, dynamic_ncols=True, From bc107787cc490eaca545e1a9876022272e209931 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 11:45:47 -0800 Subject: [PATCH 13/24] Clean up get_scenes_to_render --- manimlib/extract_scene.py | 63 +++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 35 deletions(-) diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index 8f1a36ab..101c5725 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -61,15 +61,13 @@ def get_scene_config(config): } -def compute_total_frames(scene_class, scene_config, config): +def compute_total_frames(scene_class, scene_config): """ When a scene is being written to file, a copy of the scene is run with skip_animations set to true so as to count how many frames it will require. This allows for a total progress bar on rendering, and also allows runtime errors to be exposed preemptively for long running scenes. """ - if not config["prerun"]: - return -1 pre_config = copy.deepcopy(scene_config) pre_config["file_writer_config"]["write_to_movie"] = False pre_config["file_writer_config"]["save_last_frame"] = False @@ -81,40 +79,35 @@ def compute_total_frames(scene_class, scene_config, config): return int(total_time * scene_config["camera_config"]["fps"]) -def get_scenes_to_render(scene_classes, scene_config, config): - if config["write_all"]: - return [sc(**scene_config) for sc in scene_classes] +def scene_from_class(scene_class, scene_config, config): + fw_config = scene_config["file_writer_config"] + if fw_config["write_to_movie"] and config["prerun"]: + fw_config["total_frames"] = compute_total_frames(scene_class, scene_config) + return scene_class(**scene_config) - result = [] - for scene_name in config["scene_names"]: - found = False - for scene_class in scene_classes: - if scene_class.__name__ == scene_name: - fw_config = scene_config["file_writer_config"] - if fw_config["write_to_movie"]: - fw_config["total_frames"] = compute_total_frames(scene_class, scene_config, config) - scene = scene_class(**scene_config) - result.append(scene) - found = True - break - if not found and (scene_name != ""): - log.error(f"No scene named {scene_name} found") - if result: - return result - - # another case - result=[] - if len(scene_classes) == 1: - scene_classes = [scene_classes[0]] + +def get_scenes_to_render(all_scene_classes, scene_config, config): + if config["write_all"]: + return [sc(**scene_config) for sc in all_scene_classes] + + names_to_classes = {sc.__name__ : sc for sc in all_scene_classes} + scene_names = config["scene_names"] + + for name in set.difference(set(scene_names), names_to_classes): + log.error(f"No scene named {name} found") + scene_names.remove(name) + + if scene_names: + classes_to_run = [names_to_classes[name] for name in scene_names] + elif len(all_scene_classes) == 1: + classes_to_run = [all_scene_classes[0]] else: - scene_classes = prompt_user_for_choice(scene_classes) - for scene_class in scene_classes: - fw_config = scene_config["file_writer_config"] - if fw_config["write_to_movie"]: - fw_config["total_frames"] = compute_total_frames(scene_class, scene_config, config) - scene = scene_class(**scene_config) - result.append(scene) - return result + classes_to_run = prompt_user_for_choice(all_scene_classes) + + return [ + scene_from_class(scene_class, scene_config, config) + for scene_class in classes_to_run + ] def get_scene_classes_from_module(module): From 12dc124d72c264a159599a6531a7d3cdca080363 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 11:46:03 -0800 Subject: [PATCH 14/24] Revert to simple progress_description_len default --- manimlib/scene/scene_file_writer.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index bd2e9b19..72d076e8 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -46,7 +46,7 @@ class SceneFileWriter(object): show_file_location_upon_completion: bool = False, quiet: bool = False, total_frames: int = 0, - progress_description_len: int | None = None, + progress_description_len: int = 40, ): self.scene: Scene = scene self.write_to_movie = write_to_movie @@ -62,8 +62,7 @@ class SceneFileWriter(object): self.show_file_location_upon_completion = show_file_location_upon_completion self.quiet = quiet self.total_frames = total_frames - self.progress_description_len = progress_description_len or \ - 40 if total_frames > 0 else 80 + self.progress_description_len = progress_description_len # State during file writing self.writing_process: sp.Popen | None = None From e1bb360e0bfffbebb562fa6ca722d87e16c5b643 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 12:46:01 -0800 Subject: [PATCH 15/24] Add CLI args for setting video codec and pixel forma --- manimlib/config.py | 22 +++++++++++++++++++++- manimlib/scene/scene_file_writer.py | 25 ++++++++++++------------- 2 files changed, 33 insertions(+), 14 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index e8a26809..bb5e57ef 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -93,6 +93,14 @@ def parse_cli(): action="store_true", help="Render to a movie file with an alpha channel", ) + parser.add_argument( + "--vcodec", + help="Video codec to use with ffmpeg", + ) + parser.add_argument( + "--pix_fmt", + help="Pixel format to use for the output of ffmpeg, defaults to `yuv420p`", + ) parser.add_argument( "-q", "--quiet", action="store_true", @@ -392,7 +400,7 @@ def get_output_directory(args: Namespace, custom_config: dict) -> str: def get_file_writer_config(args: Namespace, custom_config: dict) -> dict: - return { + result = { "write_to_movie": not args.skip_animations and args.write_file, "break_into_partial_movies": custom_config["break_into_partial_movies"], "save_last_frame": args.skip_animations and args.write_file, @@ -408,6 +416,18 @@ def get_file_writer_config(args: Namespace, custom_config: dict) -> dict: "quiet": args.quiet, } + if args.vcodec: + result["video_codec"] = args.vcodec + elif args.transparent: + result["video_codec"] = 'prores_ks' + elif args.gif: + result["video_codec"] = '' + + if args.pix_fmt: + result["pix_fmt"] = args.pix_fmt + + return result + def get_window_config(args: Namespace, custom_config: dict, camera_config: dict) -> dict: # Default to making window half the screen size diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 72d076e8..8b994098 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -47,6 +47,8 @@ class SceneFileWriter(object): quiet: bool = False, total_frames: int = 0, progress_description_len: int = 40, + video_codec: str = "libx264", + pixel_format: str = "yuvj422p", ): self.scene: Scene = scene self.write_to_movie = write_to_movie @@ -63,6 +65,8 @@ class SceneFileWriter(object): self.quiet = quiet self.total_frames = total_frames self.progress_description_len = progress_description_len + self.video_codec = video_codec + self.pixel_format = pixel_format # State during file writing self.writing_process: sp.Popen | None = None @@ -262,19 +266,10 @@ class SceneFileWriter(object): '-an', # Tells FFMPEG not to expect any audio '-loglevel', 'error', ] - if self.movie_file_extension == ".mov": - # This is if the background of the exported - # video should be transparent. - command += [ - '-vcodec', 'prores_ks', - ] - elif self.movie_file_extension == ".gif": - command += [] - else: - command += [ - '-vcodec', 'libx264', - '-pix_fmt', 'yuv420p', - ] + if self.video_codec: + command += ['-vcodec', self.video_codec] + if self.pixel_format: + command += ['-pix_fmt', self.pixel_format] command += [self.temp_file_path] self.writing_process = sp.Popen(command, stdin=sp.PIPE) @@ -287,6 +282,10 @@ class SceneFileWriter(object): ) self.set_progress_display_description() + def use_fast_encoding(self): + self.video_codec = "libx264rgb" + self.pixel_format = "rgb32" + def begin_insert(self): # Begin writing process self.write_to_movie = True From a54d1eddfc30ddff93a25dec42dfc2c96693b663 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 12:48:56 -0800 Subject: [PATCH 16/24] Set default pixel format to yuv420p --- manimlib/scene/scene_file_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 8b994098..fdcabb8f 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -48,7 +48,7 @@ class SceneFileWriter(object): total_frames: int = 0, progress_description_len: int = 40, video_codec: str = "libx264", - pixel_format: str = "yuvj422p", + pixel_format: str = "yuv420p", ): self.scene: Scene = scene self.write_to_movie = write_to_movie From b8fe7b0172106a6475e5a12ca55f7255064c1ca3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 17:28:00 -0800 Subject: [PATCH 17/24] Note that restoring state affects the mobject list --- manimlib/scene/scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index d2af205a..b3b1db20 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -726,6 +726,7 @@ class Scene(object): def get_state(self) -> SceneState: return SceneState(self) + @affects_mobject_list def restore_state(self, scene_state: SceneState): scene_state.restore_scene(self) From fab917cceeaae088f28b53531ce2e3922d883008 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 17:28:27 -0800 Subject: [PATCH 18/24] Improve TransformMatchingString to match longest common substrings by default --- .../animation/transform_matching_parts.py | 70 ++++++++++++++----- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/manimlib/animation/transform_matching_parts.py b/manimlib/animation/transform_matching_parts.py index b832cf0e..7e30d2ad 100644 --- a/manimlib/animation/transform_matching_parts.py +++ b/manimlib/animation/transform_matching_parts.py @@ -1,19 +1,15 @@ from __future__ import annotations import itertools as it - -import numpy as np +from difflib import SequenceMatcher from manimlib.animation.composition import AnimationGroup from manimlib.animation.fading import FadeInFromPoint from manimlib.animation.fading import FadeOutToPoint -from manimlib.animation.fading import FadeTransformPieces from manimlib.animation.transform import Transform from manimlib.mobject.mobject import Mobject -from manimlib.mobject.mobject import Group -from manimlib.mobject.svg.string_mobject import StringMobject -from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VMobject +from manimlib.mobject.svg.string_mobject import StringMobject from typing import TYPE_CHECKING @@ -131,28 +127,64 @@ class TransformMatchingStrings(TransformMatchingParts): target: StringMobject, matched_keys: Iterable[str] = [], key_map: dict[str, str] = dict(), - matched_pairs: Iterable[tuple[Mobject, Mobject]] = [], + matched_pairs: Iterable[tuple[VMobject, VMobject]] = [], **kwargs, ): - matched_pairs = list(matched_pairs) + [ - *[(source[key], target[key]) for key in matched_keys], - *[(source[key1], target[key2]) for key1, key2 in key_map.items()], - *[ - (source[substr], target[substr]) - for substr in [ - *source.get_specified_substrings(), - *target.get_specified_substrings(), - *source.get_symbol_substrings(), - *target.get_symbol_substrings(), - ] - ] + matched_pairs = [ + *matched_pairs, + *self.matching_blocks(source, target, matched_keys, key_map), ] + super().__init__( source, target, matched_pairs=matched_pairs, **kwargs, ) + def matching_blocks( + self, + source: StringMobject, + target: StringMobject, + matched_keys: Iterable[str], + key_map: dict[str, str] + ) -> list[tuple[VMobject, VMobject]]: + syms1 = source.get_symbol_substrings() + syms2 = target.get_symbol_substrings() + counts1 = list(map(source.substr_to_path_count, syms1)) + counts2 = list(map(target.substr_to_path_count, syms2)) + + # Start with user specified matches + blocks = [(source[key], target[key]) for key in matched_keys] + blocks += [(source[key1], target[key2]) for key1, key2 in key_map.items()] + + # Nullify any intersections with those matches in the two symbol lists + for sub_source, sub_target in blocks: + for i in range(len(syms1)): + if source[i] in sub_source.family_members_with_points(): + syms1[i] = "Null1" + for j in range(len(syms2)): + if target[j] in sub_target.family_members_with_points(): + syms2[j] = "Null2" + + # Group together longest matching substrings + while True: + matcher = SequenceMatcher(None, syms1, syms2) + match = matcher.find_longest_match(0, len(syms1), 0, len(syms2)) + if match.size == 0: + break + + i1 = sum(counts1[:match.a]) + i2 = sum(counts2[:match.b]) + size = sum(counts1[match.a:match.a + match.size]) + + blocks.append((source[i1:i1 + size], target[i2:i2 + size])) + + for i in range(match.size): + syms1[match.a + i] = "Null1" + syms2[match.b + i] = "Null2" + + return blocks + class TransformMatchingTex(TransformMatchingStrings): """Alias for TransformMatchingStrings""" From 3bf9e40aba0b63fee94bb5a64d2c2ae8daee821e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 17:35:20 -0800 Subject: [PATCH 19/24] Add more lenient tolerance to Mobject.has_same_shape_as --- manimlib/mobject/mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index bad88b34..2f974389 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -739,7 +739,7 @@ class Mobject(object): ) if len(points1) != len(points2): return False - return bool(np.isclose(points1, points2).all()) + return bool(np.isclose(points1, points2, atol=self.get_width() * 1e-2).all()) # Creating new Mobjects from this one From e3b95276fa5835a168d649cbb5d2a6209718b640 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 17:48:51 -0800 Subject: [PATCH 20/24] Update TexTransformExample, and pull out TexIndexing --- example_scenes.py | 84 +++++++++++++++++++++++++++++------------------ 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index 9aa3e108..127e438d 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -174,16 +174,17 @@ class TexTransformExample(Scene): self.add(lines[0]) # The animation TransformMatchingStrings will line up parts # of the source and target which have matching substring strings. - # Here, giving it a little path_arc makes each part sort of - # rotate into their final positions, which feels appropriate - # for the idea of rearranging an equation + # Here, giving it a little path_arc makes each part rotate into + # their final positions, which feels appropriate for the idea of + # rearranging an equation self.play( TransformMatchingStrings( lines[0].copy(), lines[1], # matched_keys specifies which substring should # line up. If it's not specified, the animation - # will try its best, but may not quite give the - # intended effect + # will align the longest matching substrings. + # In this case, the substring "^2 = C^2" would + # trip it up matched_keys=["A^2", "B^2", "C^2"], # When you want a substring from the source # to go to a non-equal substring from the target, @@ -206,25 +207,57 @@ class TexTransformExample(Scene): ), ) self.wait(2) - - # You can also index into Tex mobject (or other StringMobjects) - # by substrings and regular expressions - top_equation = lines[0] - low_equation = lines[3] - - self.play(LaggedStartMap(FlashAround, low_equation["C"], lag_ratio=0.5)) - self.play(LaggedStartMap(FlashAround, low_equation["B"], lag_ratio=0.5)) - self.play(LaggedStartMap(FlashAround, top_equation[re.compile(r"\w\^2")])) - self.play(Indicate(low_equation[R"\sqrt"])) - self.wait() self.play(LaggedStartMap(FadeOut, lines, shift=2 * RIGHT)) + # TransformMatchingShapes will try to line up all pieces of a + # source mobject with those of a target, regardless of the + # what Mobject type they are. + source = Text("the morse code", height=1) + target = Text("here come dots", height=1) + saved_source = source.copy() + + self.play(Write(source)) + self.wait() + kw = dict(run_time=3, path_arc=PI / 2) + self.play(TransformMatchingShapes(source, target, **kw)) + self.wait() + self.play(TransformMatchingShapes(target, saved_source, **kw)) + self.wait() + + +class TexIndexing(Scene): + def construct(self): + # You can index into Tex mobject (or other StringMobjects) by substrings + equation = Tex(R"e^{\pi i} = -1", font_size=144) + + self.add(equation) + self.play(FlashAround(equation["e"])) + self.wait() + self.play(Indicate(equation[R"\pi"])) + self.wait() + self.play(TransformFromCopy( + equation[R"e^{\pi i}"].copy().set_opacity(0.5), + equation["-1"], + path_arc=-PI / 2, + run_time=3 + )) + self.play(FadeOut(equation)) + + # Or regular expressions + equation = Tex("A^2 + B^2 = C^2", font_size=144) + + self.play(Write(equation)) + for part in equation[re.compile(r"\w\^2")]: + self.play(FlashAround(part)) + self.wait() + self.play(FadeOut(equation)) + # Indexing by substrings like this may not work when # the order in which Latex draws symbols does not match # the order in which they show up in the string. # For example, here the infinity is drawn before the sigma # so we don't get the desired behavior. - equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}") + equation = Tex(R"\sum_{n = 1}^\infty \frac{1}{n^2} = \frac{\pi^2}{6}", font_size=72) self.play(FadeIn(equation)) self.play(equation[R"\infty"].animate.set_color(RED)) # Doesn't hit the infinity self.wait() @@ -236,27 +269,14 @@ class TexTransformExample(Scene): equation = Tex( R"\sum_{n = 1}^\infty {1 \over n^2} = {\pi^2 \over 6}", # Explicitly mark "\infty" as a substring you might want to access - isolate=[R"\infty"] + isolate=[R"\infty"], + font_size=72 ) self.play(FadeIn(equation)) self.play(equation[R"\infty"].animate.set_color(RED)) # Got it! self.wait() self.play(FadeOut(equation)) - # TransformMatchingShapes will try to line up all pieces of a - # source mobject with those of a target, regardless of the - # what Mobject type they are. - source = Text("the morse code", height=1) - target = Text("here come dots", height=1) - - self.play(Write(source)) - self.wait() - kw = dict(run_time=3, path_arc=PI / 2) - self.play(TransformMatchingShapes(source, target, **kw)) - self.wait() - self.play(TransformMatchingShapes(target, source, **kw)) - self.wait() - class UpdatersExample(Scene): def construct(self): From c918e8478441b1b1a4d3bc7592283b825be45017 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Fri, 3 Feb 2023 18:10:29 -0800 Subject: [PATCH 21/24] Change default progress bar format --- manimlib/scene/scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index b3b1db20..ed4bd5fc 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -557,6 +557,7 @@ class Scene(object): leave=self.leave_progress_bars, ascii=True if platform.system() == 'Windows' else None, desc=desc, + bar_format="{l_bar} {n_fmt:3}/{total_fmt:3} {rate_fmt}{postfix}", ) else: return times From 7d1330fa68109b339920aa58b8ecb066c6fa1f09 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 4 Feb 2023 16:49:32 -0800 Subject: [PATCH 22/24] Check if mobject_uniforms is None --- manimlib/shader_wrapper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index d47cb92f..4e811147 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -52,7 +52,7 @@ class ShaderWrapper(object): self.render_primitive = render_primitive self.program_uniform_mirror: UniformDict = dict() - self.bind_to_mobject_uniforms(mobject_uniforms) + self.bind_to_mobject_uniforms(mobject_uniforms or dict()) self.init_program_code() self.init_program() From 4e90a77fcdb1a886078561d57fb169d8f552442f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 4 Feb 2023 16:50:12 -0800 Subject: [PATCH 23/24] Change type hint on LaggedStart to accept any functions outputting animations --- manimlib/animation/composition.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index a1c0ed37..e4ad5656 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -165,7 +165,7 @@ class LaggedStart(AnimationGroup): class LaggedStartMap(LaggedStart): def __init__( self, - AnimationClass: type, + anim_func: Callable[[Mobject], Animation], group: Mobject, arg_creator: Callable[[Mobject], tuple] | None = None, run_time: float = 2.0, @@ -175,7 +175,7 @@ class LaggedStartMap(LaggedStart): anim_kwargs = dict(kwargs) anim_kwargs.pop("lag_ratio", None) super().__init__( - *(AnimationClass(submob, **anim_kwargs) for submob in group), + *(anim_func(submob, **anim_kwargs) for submob in group), run_time=run_time, lag_ratio=lag_ratio, ) From d1b1df64a546e7987bf5bc3822800436bdf10b97 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 4 Feb 2023 16:51:14 -0800 Subject: [PATCH 24/24] Ensure Window's scene always points back to window Issues can arise in the few milliseconds of startup otherwise. --- manimlib/window.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/window.py b/manimlib/window.py index 9bc5090a..299a4c8d 100644 --- a/manimlib/window.py +++ b/manimlib/window.py @@ -29,6 +29,7 @@ class Window(PygletWindow): size: tuple[int, int] = (1280, 720), samples = 0 ): + scene.window = self super().__init__(size=size, samples=samples) self.default_size = size