From 04d3e6a47ca9960777754aa3abe77f5acc87b5a0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 16:07:28 -0800 Subject: [PATCH 01/14] Interpolate colors using square of rgbs --- manimlib/utils/color.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manimlib/utils/color.py b/manimlib/utils/color.py index d35a40ce..6bd49522 100644 --- a/manimlib/utils/color.py +++ b/manimlib/utils/color.py @@ -88,7 +88,7 @@ def color_gradient( alphas_mod1[-1] = 1 floors[-1] = len(rgbs) - 2 return [ - rgb_to_color(interpolate(rgbs[i], rgbs[i + 1], alpha)) + rgb_to_color(np.sqrt(interpolate(rgbs[i]**2, rgbs[i + 1]**2, alpha))) for i, alpha in zip(floors, alphas_mod1) ] @@ -98,13 +98,13 @@ def interpolate_color( color2: ManimColor, alpha: float ) -> Color: - rgb = interpolate(color_to_rgb(color1), color_to_rgb(color2), alpha) + rgb = np.sqrt(interpolate(color_to_rgb(color1)**2, color_to_rgb(color2)**2, alpha)) return rgb_to_color(rgb) def average_color(*colors: ManimColor) -> Color: rgbs = np.array(list(map(color_to_rgb, colors))) - return rgb_to_color(rgbs.mean(0)) + return rgb_to_color(np.sqrt((rgbs**2).mean(0))) def random_color() -> Color: From 0f9adbf91c806707c68e66bd3815a7d3405643a6 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 18:52:00 -0800 Subject: [PATCH 02/14] Add Tex.make_number_changable --- manimlib/mobject/svg/tex_mobject.py | 55 ++++++++++++++++++++++++++--- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index 77a438b4..27e51df3 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -212,16 +212,61 @@ class Tex(StringMobject): ): return self.set_parts_color_by_dict(color_map) + def get_tex(self) -> str: + return self.get_string() + + # Specific to Tex def substr_to_path_count(self, substr: str) -> int: tex = self.get_tex() if len(self) != num_tex_symbols(tex): - log.warning( - f"Estimated size of {tex} does not match true size", - ) + log.warning(f"Estimated size of {tex} does not match true size") return num_tex_symbols(substr) - def get_tex(self) -> str: - return self.get_string() + def make_number_changable( + self, + value: float | int | str, + index: int = 0, + replace_all: bool = False, + **config, + ) -> VMobject: + substr = str(value) + parts = self.select_parts(substr) + if len(parts) == 0: + log.warning(f"{value} not found in Tex.make_number_changable call") + return VMobject() + if index > len(parts) - 1: + log.warning(f"Requested {index}th occurance of {value}, but only {len(parts)} exist") + return VMobject() + if not replace_all: + parts = [parts[index]] + + from manimlib.mobject.numbers import DecimalNumber + + decimal_mobs = [] + for part in parts: + if "." in substr: + num_decimal_places = len(substr.split(".")[1]) + else: + num_decimal_places = 0 + decimal_mob = DecimalNumber( + float(value), + num_decimal_places=num_decimal_places, + **config, + ) + decimal_mob.replace(part) + decimal_mob.match_style(part) + if len(part) > 1: + self.remove(*part[1:]) + self.replace_submobject(self.submobjects.index(part[0]), decimal_mob) + decimal_mobs.append(decimal_mob) + + # Replace substr with something that looks like a tex command. This + # is to ensure Tex.substr_to_path_count counts it correctly. + self.string = self.string.replace(substr, R"\decimalmob", 1) + + if replace_all: + return VGroup(*decimal_mobs) + return decimal_mobs[index] class TexText(Tex): From 7df12c68dcc7253492744fd75cb402974d60afca Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 18:52:13 -0800 Subject: [PATCH 03/14] Tiny cleanup --- manimlib/utils/tex.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/utils/tex.py b/manimlib/utils/tex.py index 1a70e429..294be247 100644 --- a/manimlib/utils/tex.py +++ b/manimlib/utils/tex.py @@ -15,7 +15,7 @@ def num_tex_symbols(tex: str) -> int: # First, remove patterns like \begin{align}, \phantom{thing}, # \begin{array}{cc}, etc. pattern = "|".join( - r"(\\" + s + ")" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?" + rf"(\\{s})" + r"(\{\w+\})?(\{\w+\})?(\[\w+\])?" for s in ["begin", "end", "phantom"] ) for tup in re.findall(pattern, tex): From 5c33c7e4a8d0359cc6985b90490520e47fea5ffe Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 18:52:37 -0800 Subject: [PATCH 04/14] Remove "None" output type for set_animating_status --- 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 9fa6ebee..caf34361 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -821,7 +821,7 @@ class Mobject(object): def is_changing(self) -> bool: return self._is_animating or self.has_updaters - def set_animating_status(self, is_animating: bool, recurse: bool = True) -> None: + def set_animating_status(self, is_animating: bool, recurse: bool = True): for mob in (*self.get_family(recurse), *self.get_ancestors(extended=True)): mob._is_animating = is_animating return self From 4db01fd22183c53afa8447e24194d267a9dcf124 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 18:53:06 -0800 Subject: [PATCH 05/14] Fix Mobject.looks_identical --- manimlib/mobject/mobject.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index caf34361..23389bf0 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -667,8 +667,17 @@ class Mobject(object): if set(d1).difference(d2): return False for key in d1: - if not np.isclose(d1[key], d2[key]).all(): - return False + try: + if isinstance(d1[key], np.ndarray) and isinstance(d2[key], np.ndarray): + if not d1[key].size == d2[key].size: + return False + if not np.isclose(d1[key], d2[key]).all(): + return False + except Exception as e: + print(self) + print(d1[key]) + print(d2[key]) + raise e return True def has_same_shape_as(self, mobject: Mobject) -> bool: From 3b4a233bb144251825f148e5a72e7f6b97a736a4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 18:53:26 -0800 Subject: [PATCH 06/14] Fix LaggedStartMap --- manimlib/animation/composition.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/composition.py b/manimlib/animation/composition.py index 0acdd49c..9e6beac4 100644 --- a/manimlib/animation/composition.py +++ b/manimlib/animation/composition.py @@ -163,12 +163,13 @@ class LaggedStartMap(LaggedStart): group: Mobject, arg_creator: Callable[[Mobject], tuple] | None = None, run_time: float = 2.0, + lag_ratio: float = DEFAULT_LAGGED_START_LAG_RATIO, **kwargs ): anim_kwargs = dict(kwargs) anim_kwargs.pop("lag_ratio", None) super().__init__( *(AnimationClass(submob, **anim_kwargs) for submob in group), - group=group, run_time=run_time, + lag_ratio=lag_ratio, ) From 5d87f3f9545f388c7a3ccd841b344b44ef7e6da8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 19:42:54 -0800 Subject: [PATCH 07/14] Update parents of new_mob in Mobject.replace --- manimlib/mobject/mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 23389bf0..5e031e97 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -394,6 +394,7 @@ class Mobject(object): if self in old_submob.parents: old_submob.parents.remove(self) self.submobjects[index] = new_submob + new_submob.parents.append(self) self.assemble_family() return self From fdccfd51fcc45dc7b6b10e2c331835dca1f1f305 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 19:43:12 -0800 Subject: [PATCH 08/14] Add TexAndNumbersExample --- example_scenes.py | 68 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/example_scenes.py b/example_scenes.py index c1a2fa50..f005cecf 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -453,6 +453,74 @@ class GraphExample(Scene): self.wait() +class TexAndNumbersExample(InteractiveScene): + def construct(self): + axes = Axes((-3, 3), (-3, 3), unit_size=1) + axes.to_edge(DOWN) + axes.add_coordinate_labels(font_size=16) + circle = Circle(radius=2) + circle.set_stroke(YELLOW, 3) + circle.move_to(axes.get_origin()) + self.add(axes, circle) + + # When numbers show up in tex, they can be readily + # replaced with DecimalMobjects so that methods like + # get_value and set_value can be called on them, and + # animations like ChangeDecimalToValue can be called + # on them. + tex = Tex("x^2 + y^2 = 4.00") + tex.next_to(axes, UP, buff=0.5) + value = tex.make_number_changable("4.00") + + + # This will tie the right hand side of our equation to + # the square of the radius of the circle + value.add_updater(lambda v: v.set_value(circle.get_radius()**2)) + self.add(tex) + + text = Text(""" + You can manipulate numbers + in Tex mobjects + """, font_size=30) + text.next_to(tex, RIGHT, buff=1.5) + arrow = Arrow(text, tex) + self.add(text, arrow) + + self.play( + circle.animate.set_height(2.0), + run_time=4, + rate_func=there_and_back, + ) + + # By default, tex.make_number_changable replaces the first occurance + # of the number,but by passing replace_all=True it replaces all and + # returns a group of the results + exponents = tex.make_number_changable("2", replace_all=True) + self.play( + LaggedStartMap( + FlashAround, exponents, + lag_ratio=0.2, buff=0.1, color=RED + ), + exponents.animate.set_color(RED) + ) + + def func(x, y): + # Switch from manim coords to axes coords + xa, ya = axes.point_to_coords(np.array([x, y, 0])) + return xa**4 + ya**4 - 4 + + new_curve = ImplicitFunction(func) + new_curve.match_style(circle) + circle.rotate(angle_of_vector(new_curve.get_start())) # Align + value.clear_updaters() + + self.play( + *(ChangeDecimalToValue(exp, 4) for exp in exponents), + ReplacementTransform(circle.copy(), new_curve), + circle.animate.set_stroke(width=1, opacity=0.5), + ) + + class SurfaceExample(Scene): default_camera_config = dict(samples=4) From 632819dd6dc547a37dc497d1d2f1a27e54d26737 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 19:45:29 -0800 Subject: [PATCH 09/14] Show examples of indexing by substrings and regular expressions --- example_scenes.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index f005cecf..f9268aae 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -193,11 +193,7 @@ class TexTransformExample(Scene): ), ) self.wait() - self.play( - TransformMatchingStrings( - lines[1].copy(), lines[2], - ), - ) + self.play(TransformMatchingStrings(lines[1].copy(), lines[2])) self.wait() self.play( TransformMatchingStrings( @@ -208,6 +204,17 @@ 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 From 8e3378f798c5155b38e4b8c757717b4bb3504d89 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 20:17:54 -0800 Subject: [PATCH 10/14] Orient Camera.get_pixel_array correctly --- manimlib/camera/camera.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index be3dbd14..efd8c537 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -305,7 +305,8 @@ class Camera(object): def get_pixel_array(self) -> np.ndarray: raw = self.get_raw_fbo_data(dtype='f4') flat_arr = np.frombuffer(raw, dtype='f4') - arr = flat_arr.reshape([*self.fbo.size, self.n_channels]) + arr = flat_arr.reshape([*reversed(self.fbo.size), self.n_channels]) + arr = arr[::-1] # Convert from float return (self.rgb_max_val * arr).astype(self.pixel_array_dtype) From 7dde368eebf12f7fe33889ae5f1a2cc05cab384b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 20:18:19 -0800 Subject: [PATCH 11/14] Ensure error-flash rect is fixed in frame --- manimlib/scene/scene.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 55b8c37d..10940292 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -261,10 +261,9 @@ class Scene(object): shell.showtraceback((etype, evalue, tb), tb_offset=tb_offset) if self.embed_error_sound: os.system("printf '\a'") - self.play(VFadeInThenOut( - FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0), - run_time=0.5, - )) + rect = FullScreenRectangle().set_stroke(RED, 30).set_fill(opacity=0) + rect.fix_in_frame() + self.play(VFadeInThenOut(rect, run_time=0.5)) shell.set_custom_exc((Exception,), custom_exc) From 7895a2cfee645d8f6538e78fcf169f875b832719 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 20:51:00 -0800 Subject: [PATCH 12/14] No need to immediately compute triangulation for SVG paths --- manimlib/mobject/svg/svg_mobject.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/mobject/svg/svg_mobject.py b/manimlib/mobject/svg/svg_mobject.py index f8d9bc30..802eb04b 100644 --- a/manimlib/mobject/svg/svg_mobject.py +++ b/manimlib/mobject/svg/svg_mobject.py @@ -323,7 +323,6 @@ class VMobjectFromSVGPath(VMobject): self.set_points(self.get_points_without_null_curves()) # Save to a file for future use np.save(points_filepath, self.get_points()) - self.get_triangulation() def handle_commands(self) -> None: segment_class_to_func_map = { From dddaef0e6ce5af477db2b08eaaab24484f9a9cd7 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 20:51:11 -0800 Subject: [PATCH 13/14] Add lru_cache for get_shader_code_from_file --- manimlib/shader_wrapper.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manimlib/shader_wrapper.py b/manimlib/shader_wrapper.py index 13fb3a28..8728833a 100644 --- a/manimlib/shader_wrapper.py +++ b/manimlib/shader_wrapper.py @@ -7,6 +7,8 @@ import re import moderngl import numpy as np +from functools import lru_cache + from manimlib.utils.directories import get_shader_dir from manimlib.utils.file_ops import find_file from manimlib.utils.iterables import resize_array @@ -159,7 +161,7 @@ class ShaderWrapper(object): # For caching filename_to_code_map: dict[str, str] = {} - +@lru_cache(maxsize=12) def get_shader_code_from_file(filename: str) -> str | None: if not filename: return None From 66f0a57c6be2bfa88e5edcb371ba573d2da83c07 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 29 Dec 2022 20:58:37 -0800 Subject: [PATCH 14/14] Clean up looks_identical --- manimlib/mobject/mobject.py | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 5e031e97..01e7fe2a 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -668,17 +668,11 @@ class Mobject(object): if set(d1).difference(d2): return False for key in d1: - try: - if isinstance(d1[key], np.ndarray) and isinstance(d2[key], np.ndarray): - if not d1[key].size == d2[key].size: - return False - if not np.isclose(d1[key], d2[key]).all(): + if isinstance(d1[key], np.ndarray) and isinstance(d2[key], np.ndarray): + if not d1[key].size == d2[key].size: return False - except Exception as e: - print(self) - print(d1[key]) - print(d2[key]) - raise e + if not np.isclose(d1[key], d2[key]).all(): + return False return True def has_same_shape_as(self, mobject: Mobject) -> bool: