From b920e7be7b85bc0bb0577e2f71c4320bb97b42d4 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 25 Apr 2022 10:45:35 -0700 Subject: [PATCH 01/58] Rewrite remove_list_redundancies based on (ordered) dicts --- manimlib/utils/iterables.py | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/manimlib/utils/iterables.py b/manimlib/utils/iterables.py index bdaa76c2..2b795ea7 100644 --- a/manimlib/utils/iterables.py +++ b/manimlib/utils/iterables.py @@ -13,19 +13,12 @@ if TYPE_CHECKING: S = TypeVar("S") -def remove_list_redundancies(l: Iterable[T]) -> list[T]: +def remove_list_redundancies(lst: Iterable[T]) -> list[T]: """ Used instead of list(set(l)) to maintain order Keeps the last occurrence of each element """ - reversed_result = [] - used = set() - for x in reversed(l): - if x not in used: - reversed_result.append(x) - used.add(x) - reversed_result.reverse() - return reversed_result + return list(reversed(dict.fromkeys(reversed(lst)))) def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]: @@ -33,7 +26,7 @@ def list_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]: Used instead of list(set(l1).update(l2)) to maintain order, making sure duplicates are removed from l1, not l2. """ - return [e for e in l1 if e not in l2] + list(l2) + return remove_list_redundancies([*l1, *l2]) def list_difference_update(l1: Iterable[T], l2: Iterable[T]) -> list[T]: From 2dcc989bb48ebe82c7429b9bfa37cc1354812fbf Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:51:43 -0700 Subject: [PATCH 02/58] (whitespace) --- manimlib/mobject/functions.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/functions.py b/manimlib/mobject/functions.py index 9ec232a7..6d55b395 100644 --- a/manimlib/mobject/functions.py +++ b/manimlib/mobject/functions.py @@ -75,6 +75,7 @@ class ParametricCurve(VMobject): if hasattr(self, "x_range"): return self.x_range + class FunctionGraph(ParametricCurve): CONFIG = { "color": YELLOW, From c498b8875081968ee015235919fc5974918c7cf8 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:52:27 -0700 Subject: [PATCH 03/58] Small tweaks to Mobject.looks_identical for marginal speed --- manimlib/mobject/mobject.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 1e04a960..ef9f21ff 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -619,8 +619,8 @@ class Mobject(object): return self def looks_identical(self, mobject: Mobject): - fam1 = self.get_family() - fam2 = mobject.get_family() + fam1 = self.family_members_with_points() + fam2 = mobject.family_members_with_points() if len(fam1) != len(fam2): return False for m1, m2 in zip(fam1, fam2): @@ -628,11 +628,13 @@ class Mobject(object): if set(d1).difference(d2): return False for key in d1: - if isinstance(d1[key], np.ndarray): - if not np.all(d1[key] == d2[key]): + eq = (d1[key] == d2[key]) + if isinstance(eq, bool): + if not eq: + return False + else: + if not eq.all(): return False - elif d1[key] != d2[key]: - return False return True # Creating new Mobjects from this one From 0e45b41fea5f22d136f62f4af2e0d892e61a12ce Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:52:44 -0700 Subject: [PATCH 04/58] Match updaters in Mobject.become --- manimlib/mobject/mobject.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index ef9f21ff..58fda4b1 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -616,6 +616,7 @@ class Mobject(object): sm1.depth_test = sm2.depth_test sm1.render_primitive = sm2.render_primitive self.refresh_bounding_box(recurse_down=True) + self.match_updaters(mobject) return self def looks_identical(self, mobject: Mobject): From ec9ed32d782e98d9df1154ca13e2823a82171289 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:53:23 -0700 Subject: [PATCH 05/58] Organize get_ancestors from top to bottom --- manimlib/mobject/mobject.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 58fda4b1..354ff575 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -359,8 +359,9 @@ class Mobject(object): if p not in excluded: ancestors.append(p) to_process.append(p) - # Remove redundancies while preserving order + # Ensure mobjects highest in the hierarchy show up first ancestors.reverse() + # Remove list redundancies while preserving order return list(dict.fromkeys(ancestors)) def add(self, *mobjects: Mobject): From 1b589e336f8151f2914ff00e8956baea8a95abc5 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:53:56 -0700 Subject: [PATCH 06/58] Add checkpoints to Scene --- manimlib/scene/scene.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a325bd49..c2f0f166 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -86,6 +86,7 @@ class Scene(object): 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() if self.start_at_animation_number is not None: self.skip_animations = True @@ -175,6 +176,28 @@ class Scene(object): ] }) + # This is useful if one wants to re-run a block of scene + # code, while developing, tweaking it each time. + # As long as the copied selection starts with a comment, + # this will revert to the state of the scene at the first + # point of running. + def checkpoint_paste(skip=False): + pasted = pyperclip.paste() + line0 = pasted.lstrip().split("\n")[0] + if line0.startswith("#"): + if line0 not in self.checkpoint_states: + self.checkpoint(line0) + else: + self.revert_to_checkpoint(line0) + if skip: + originally_skip = self.skip_animations + self.skip_animations = True + shell.run_line_magic("paste", "") + if skip: + self.skip_animations = originally_skip + + local_ns['checkpoint_paste'] = checkpoint_paste + # Enables gui interactions during the embed def inputhook(context): while not context.input_is_ready(): @@ -699,6 +722,18 @@ class Scene(object): self.restore_state(self.redo_stack.pop()) self.refresh_static_mobjects() + def checkpoint(self, key: str): + self.checkpoint_states[key] = self.get_state()[0] + + def revert_to_checkpoint(self, key: str): + if key not in self.checkpoint_states: + log.error(f"No checkpoint at {key}") + return + self.restore_state(self.checkpoint_states[key]) + + def clear_checkpoints(self): + self.checkpoint_states = dict() + def save_mobject_to_file(self, mobject: Mobject, file_path: str | None = None) -> None: if file_path is None: file_path = self.file_writer.get_saved_mobject_path(mobject) From d75439a60ef3fdd84a79102c911b3205ff511919 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:54:29 -0700 Subject: [PATCH 07/58] Hacky fix to lambda namespace issues with IPython embeds --- manimlib/scene/scene.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index c2f0f166..00584256 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -4,6 +4,7 @@ from functools import wraps import inspect import os import platform +import pyperclip import random import time @@ -207,9 +208,18 @@ class Scene(object): pt_inputhooks.register("manim", inputhook) shell.enable_gui("manim") + # This is hacky, but there's an issue with ipython which is that + # when you define lambda's or list comprehensions during a shell session, + # they are not aware of local variables in the surrounding scope. Because + # That comes up a fair bit during scene construction, to get around this, + # we (admittedly sketchily) update the global namespace to match the local + # namespace, since this is just a shell session anyway. + shell.events.register("pre_run_cell", lambda: shell.user_global_ns.update(shell.user_ns)) + # Operation to run after each ipython command - def post_cell_func(*args, **kwargs): + def post_cell_func(): self.refresh_static_mobjects() + self.update_frame(dt=0, ignore_skipping=True) self.save_state() shell.events.register("post_run_cell", post_cell_func) From 2b104a46fde1e9f75486f0213f1cc5be183278b1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:55:15 -0700 Subject: [PATCH 08/58] Add error message to LatexError --- manimlib/utils/tex_file_writing.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/manimlib/utils/tex_file_writing.py b/manimlib/utils/tex_file_writing.py index 557d08be..e700de9d 100644 --- a/manimlib/utils/tex_file_writing.py +++ b/manimlib/utils/tex_file_writing.py @@ -90,11 +90,13 @@ def tex_to_dvi(tex_file: str) -> str: if exit_code != 0: log_file = tex_file.replace(".tex", ".log") log.error("LaTeX Error! Not a worry, it happens to the best of us.") + error_str = "" with open(log_file, "r", encoding="utf-8") as file: for line in file.readlines(): if line.startswith("!"): - log.debug(f"The error could be: `{line[2:-1]}`") - raise LatexError() + error_str = line[2:-1] + log.debug(f"The error could be: `{error_str}`") + raise LatexError(error_str) return result From c311204993196b01cbd486e9b6ff924c07488062 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:55:29 -0700 Subject: [PATCH 09/58] Tweaks to VHighlight --- manimlib/mobject/types/vectorized_mobject.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 92363ae0..a8a8af7c 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1204,15 +1204,15 @@ class VHighlight(VGroup): vmobject: VMobject, n_layers: int = 3, color_bounds: tuple[ManimColor] = (GREY_C, GREY_E), - max_stroke_width: float = 10.0, + max_stroke_addition: float = 5.0, ): outline = vmobject.replicate(n_layers) outline.set_fill(opacity=0) - added_widths = np.linspace(0, max_stroke_width, n_layers + 1)[1:] + added_widths = np.linspace(0, max_stroke_addition, n_layers + 1)[1:] colors = color_gradient(color_bounds, n_layers) for part, added_width, color in zip(reversed(outline), added_widths, colors): for sm in part.family_members_with_points(): - part.set_stroke( + sm.set_stroke( width=sm.get_stroke_width() + added_width, color=color, ) From 7c233123a14da309e6cf57bf81cf1150bb56a15a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 09:55:46 -0700 Subject: [PATCH 10/58] Tweaks and fixes to InteractiveScene --- manimlib/scene/interactive_scene.py | 34 +++++++++++++++++++---------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 40ec18c5..1cb1c341 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -87,7 +87,7 @@ class InteractiveScene(Scene): self.cursor_location_label, self.camera.frame ] - self.select_top_level_mobs = True + self.select_top_level_mobs = False self.regenerate_selection_search_set() self.is_selecting = False @@ -190,7 +190,7 @@ class InteractiveScene(Scene): if mobject.get_depth() < 1e-2: vects = [DL, UL, UR, DR] else: - vects = list(it.product(*3 * [[-1, 1]])) + vects = np.array(list(it.product(*3 * [[-1, 1]]))) dots.add_updater(lambda d: d.set_points([ mobject.get_corner(v) + v * radius for v in vects @@ -200,7 +200,7 @@ class InteractiveScene(Scene): def get_highlight(self, mobject: Mobject) -> Mobject: if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs: result = VHighlight(mobject) - result.add_updater(lambda m: m.replace(mobject)) + result.add_updater(lambda m: m.replace(mobject, stretch=True)) return result else: return self.get_corner_dots(mobject) @@ -212,12 +212,15 @@ class InteractiveScene(Scene): self.get_highlight(mob) for mob in self.selection ]) - index = min(( - i for i, mob in enumerate(self.mobjects) - for sm in self.selection - if sm in mob.get_family() - )) - self.mobjects.insert(index, self.selection_highlight) + try: + index = min(( + i for i, mob in enumerate(self.mobjects) + for sm in self.selection + if sm in mob.get_family() + )) + self.mobjects.insert(index, self.selection_highlight) + except ValueError: + pass def add_to_selection(self, *mobjects): mobs = list(filter( @@ -260,13 +263,15 @@ class InteractiveScene(Scene): def disable_interaction(self, *mobjects: Mobject): for mob in mobjects: - self.unselectables.append(mob) + for sm in mob.get_family(): + self.unselectables.append(sm) self.regenerate_selection_search_set() def enable_interaction(self, *mobjects: Mobject): for mob in mobjects: - if mob in self.unselectables: - self.unselectables.remove(mob) + for sm in mob.get_family(): + if sm in self.unselectables: + self.unselectables.remove(sm) # Functions for keyboard actions @@ -323,6 +328,9 @@ class InteractiveScene(Scene): for mob in reversed(self.get_selection_search_set()): if self.selection_rectangle.is_touching(mob): additions.append(mob) + # If it was just a click, not a drag, add only one item + if self.selection_rectangle.get_width() < 1e-3: + break self.add_to_selection(*additions) def prepare_grab(self): @@ -445,6 +453,8 @@ class InteractiveScene(Scene): self.selection.set_y(diff[1]) def handle_resizing(self, point: np.ndarray): + if not hasattr(self, "scale_about_point"): + return vect = point - self.scale_about_point if self.window.is_key_pressed(CTRL_SYMBOL): for i in (0, 1): From 52259af5df619d3f44fbaff4c43402b93d01be2f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 11:19:20 -0700 Subject: [PATCH 11/58] Don't show animation progress bar by default --- manimlib/config.py | 6 ++++++ manimlib/scene/scene.py | 27 ++++++++++++++++----------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index 5bd6173f..48c3a186 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -148,6 +148,11 @@ def parse_cli(): action="store_true", help="Leave progress bars displayed in terminal", ) + parser.add_argument( + "--show_animation_progress", + action="store_true", + help="Show progress bar for each animation", + ) parser.add_argument( "--video_dir", help="Directory to write video", @@ -365,6 +370,7 @@ def get_configuration(args): "preview": not write_file, "presenter_mode": args.presenter_mode, "leave_progress_bars": args.leave_progress_bars, + "show_animation_progress": args.show_animation_progress, } # Camera configuration diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 00584256..ed8f32ac 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -62,6 +62,7 @@ class Scene(object): "leave_progress_bars": False, "preview": True, "presenter_mode": False, + "show_animation_progress": False, "linger_after_completion": True, "pan_sensitivity": 3, "max_num_saved_states": 50, @@ -88,8 +89,11 @@ class Scene(object): self.skip_time: float = 0 self.original_skipping_status: bool = self.skip_animations self.checkpoint_states: dict[str, list[tuple[Mobject, Mobject]]] = dict() + 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() @@ -446,21 +450,22 @@ class Scene(object): ) -> list[float] | np.ndarray | ProgressDisplay: if self.skip_animations and not override_skip_animations: return [run_time] - else: - step = 1 / self.camera.frame_rate - times = np.arange(0, run_time, step) + + times = np.arange(0, run_time, 1 / self.camera.frame_rate) if self.file_writer.has_progress_display: self.file_writer.set_progress_display_subdescription(desc) - return times - return ProgressDisplay( - times, - total=n_iterations, - leave=self.leave_progress_bars, - ascii=True if platform.system() == 'Windows' else None, - desc=desc, - ) + if self.show_animation_progress: + return ProgressDisplay( + times, + total=n_iterations, + leave=self.leave_progress_bars, + ascii=True if platform.system() == 'Windows' else None, + desc=desc, + ) + else: + return times def get_run_time(self, animations: Iterable[Animation]) -> float: return np.max([animation.run_time for animation in animations]) From e83ad785caaa1a1456e07b23f207469d335bbc0d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 27 Apr 2022 11:19:44 -0700 Subject: [PATCH 12/58] Handle quitting during scene more gracefully --- manimlib/scene/scene.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index ed8f32ac..9ca74002 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -100,6 +100,7 @@ class Scene(object): self.mouse_drag_point = Point() self.hold_on_wait = self.presenter_mode self.inside_embed = False + self.quit_interaction = False # Much nicer to work with deterministic scenes if self.random_seed is not None: @@ -117,8 +118,8 @@ class Scene(object): self.setup() try: self.construct() - except EndSceneEarlyException: - pass + except (EndSceneEarlyException, KeyboardInterrupt): + self.linger_after_completion = False self.tear_down() def setup(self) -> None: @@ -137,8 +138,12 @@ class Scene(object): def tear_down(self) -> None: self.stop_skipping() self.file_writer.finish() - if self.window and self.linger_after_completion: - self.interact() + if self.window: + if self.linger_after_completion: + self.interact() + else: + self.window.destroy() + self.window = None def interact(self) -> None: # If there is a window, enter a loop @@ -149,12 +154,12 @@ class Scene(object): " and the mouse to interact with the scene. Just press `command + q` or `esc`" " if you want to quit." ) - self.quit_interaction = False self.refresh_static_mobjects() - while not (self.window.is_closing or self.quit_interaction): - self.update_frame(1 / self.camera.frame_rate) - if self.window.is_closing: - self.window.destroy() + try: + while True: + self.update_frame(1 / self.camera.frame_rate) + except (EndSceneEarlyException, KeyboardInterrupt): + return def embed(self, close_scene_on_exit: bool = True) -> None: if not self.preview: @@ -251,6 +256,9 @@ class Scene(object): if self.skip_animations and not ignore_skipping: return + if self.window.is_closing or self.quit_interaction: + raise EndSceneEarlyException() + if self.window: self.window.clear() self.camera.clear() From ac08963feff24a1dd2e57f604b44ea0a18ab01f3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 11:59:21 -0600 Subject: [PATCH 13/58] Have selection_highlight refresh with an updater --- manimlib/scene/interactive_scene.py | 101 ++++++++++++++-------------- 1 file changed, 49 insertions(+), 52 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 1cb1c341..c585db4b 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -76,7 +76,7 @@ class InteractiveScene(Scene): def setup(self): self.selection = Group() - self.selection_highlight = Group() + self.selection_highlight = self.get_selection_highlight() self.selection_rectangle = self.get_selection_rectangle() self.color_palette = self.get_color_palette() self.cursor_location_label = self.get_cursor_location_label() @@ -87,7 +87,7 @@ class InteractiveScene(Scene): self.cursor_location_label, self.camera.frame ] - self.select_top_level_mobs = False + self.select_top_level_mobs = True self.regenerate_selection_search_set() self.is_selecting = False @@ -104,7 +104,7 @@ class InteractiveScene(Scene): rect.add_updater(self.update_selection_rectangle) return rect - def update_selection_rectangle(self, rect): + def update_selection_rectangle(self, rect: Rectangle): p1 = rect.fixed_corner p2 = self.mouse_point.get_center() rect.set_points_as_corners([ @@ -114,6 +114,33 @@ class InteractiveScene(Scene): ]) return rect + def get_selection_highlight(self): + result = Group() + result.tracked_mobjects = [] + result.add_updater(self.update_selection_highlight) + return result + + def update_selection_highlight(self, highlight: Mobject): + if set(highlight.tracked_mobjects) == set(self.selection): + return + + # Otherwise, refresh contents of highlight + highlight.tracked_mobjects = list(self.selection) + highlight.set_submobjects([ + self.get_highlight(mob) + for mob in self.selection + ]) + try: + index = min(( + i for i, mob in enumerate(self.mobjects) + for sm in self.selection + if sm in mob.get_family() + )) + self.mobjects.remove(highlight) + self.mobjects.insert(index - 1, highlight) + except ValueError: + pass + def get_color_palette(self): palette = VGroup(*( Square(fill_color=color, fill_opacity=1, side_length=1) @@ -182,7 +209,6 @@ class InteractiveScene(Scene): curr, exclude_pointless=True, ) ) - self.refresh_selection_highlight() def get_corner_dots(self, mobject: Mobject) -> Mobject: dots = DotCloud(**self.corner_dot_config) @@ -199,30 +225,17 @@ class InteractiveScene(Scene): def get_highlight(self, mobject: Mobject) -> Mobject: if isinstance(mobject, VMobject) and mobject.has_points() and not self.select_top_level_mobs: - result = VHighlight(mobject) + length = max([mobject.get_height(), mobject.get_width()]) + result = VHighlight( + mobject, + max_stroke_addition=min([50 * length, 10]), + ) result.add_updater(lambda m: m.replace(mobject, stretch=True)) return result else: return self.get_corner_dots(mobject) - def refresh_selection_highlight(self): - if len(self.selection) > 0: - self.remove(self.selection_highlight) - self.selection_highlight.set_submobjects([ - self.get_highlight(mob) - for mob in self.selection - ]) - try: - index = min(( - i for i, mob in enumerate(self.mobjects) - for sm in self.selection - if sm in mob.get_family() - )) - self.mobjects.insert(index, self.selection_highlight) - except ValueError: - pass - - def add_to_selection(self, *mobjects): + def add_to_selection(self, *mobjects: Mobject): mobs = list(filter( lambda m: m not in self.unselectables and m not in self.selection, mobjects @@ -230,31 +243,25 @@ class InteractiveScene(Scene): if len(mobs) == 0: return self.selection.add(*mobs) - self.refresh_selection_highlight() - for sm in mobs: - for mob in self.mobjects: - if sm in mob.get_family(): - mob.set_animating_status(True) - self.refresh_static_mobjects() + self.selection.set_animating_status(True) - def toggle_from_selection(self, *mobjects): + def toggle_from_selection(self, *mobjects: Mobject): for mob in mobjects: if mob in self.selection: self.selection.remove(mob) mob.set_animating_status(False) else: self.add_to_selection(mob) - self.refresh_selection_highlight() + self.refresh_static_mobjects() def clear_selection(self): for mob in self.selection: mob.set_animating_status(False) self.selection.set_submobjects([]) - self.selection_highlight.set_submobjects([]) self.refresh_static_mobjects() - def add(self, *new_mobjects: Mobject): - super().add(*new_mobjects) + def add(self, *mobjects: Mobject): + super().add(*mobjects) self.regenerate_selection_search_set() def remove(self, *mobjects: Mobject): @@ -311,10 +318,6 @@ class InteractiveScene(Scene): self.remove(*self.selection) self.clear_selection() - def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]): - super().restore_state(mobject_states) - self.refresh_selection_highlight() - def enable_selection(self): self.is_selecting = True self.add(self.selection_rectangle) @@ -328,9 +331,6 @@ class InteractiveScene(Scene): for mob in reversed(self.get_selection_search_set()): if self.selection_rectangle.is_touching(mob): additions.append(mob) - # If it was just a click, not a drag, add only one item - if self.selection_rectangle.get_width() < 1e-3: - break self.add_to_selection(*additions) def prepare_grab(self): @@ -493,15 +493,6 @@ class InteractiveScene(Scene): self.selection.set_color(mob.get_color()) self.remove(self.color_palette) - def toggle_clicked_mobject_from_selection(self, point: np.ndarray): - mob = self.point_to_mobject( - point, - search_set=self.get_selection_search_set(), - buff=SMALL_BUFF - ) - if mob is not None: - self.toggle_from_selection(mob) - def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: super().on_mouse_motion(point, d_point) if self.is_grabbing: @@ -515,7 +506,13 @@ class InteractiveScene(Scene): super().on_mouse_release(point, button, mods) if self.color_palette in self.mobjects: self.choose_color(point) - elif self.window.is_key_pressed(SHIFT_SYMBOL): - self.toggle_clicked_mobject_from_selection(point) + return + mobject = self.point_to_mobject( + point, + search_set=self.get_selection_search_set(), + buff=1e-4, + ) + if mobject is not None: + self.toggle_from_selection(mobject) else: self.clear_selection() From 5d59f945ca4d0bd5777a7bb3736170ddcbb59d2f Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 11:59:38 -0600 Subject: [PATCH 14/58] FillArrow tweak --- manimlib/mobject/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/geometry.py b/manimlib/mobject/geometry.py index 982372f6..359521b3 100644 --- a/manimlib/mobject/geometry.py +++ b/manimlib/mobject/geometry.py @@ -821,7 +821,7 @@ class FillArrow(Line): def reset_points_around_ends(self): self.set_points_by_ends( - self.get_start(), self.get_end(), path_arc=self.path_arc + self.get_start().copy(), self.get_end().copy(), path_arc=self.path_arc ) return self From c1b222c23383898b26548fe46cc9ee9a92a17bfc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 11:59:56 -0600 Subject: [PATCH 15/58] Set default buff for is_point_touching to 0 --- manimlib/mobject/mobject.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 354ff575..513f185b 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -286,7 +286,7 @@ class Mobject(object): def are_points_touching( self, points: np.ndarray, - buff: float = MED_SMALL_BUFF + buff: float = 0 ) -> bool: bb = self.get_bounding_box() mins = (bb[0] - buff) @@ -296,7 +296,7 @@ class Mobject(object): def is_point_touching( self, point: np.ndarray, - buff: float = MED_SMALL_BUFF + buff: float = 0 ) -> bool: return self.are_points_touching(np.array(point, ndmin=2), buff)[0] From a4b38fd42016d9a7186ef773b9f890c80e52ec37 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 12:14:36 -0600 Subject: [PATCH 16/58] Clean up DecimalNumber constructor --- manimlib/mobject/numbers.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 6d88c647..b51c4983 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -28,7 +28,7 @@ class DecimalNumber(VMobject): "include_background_rectangle": False, "edge_to_fix": LEFT, "font_size": 48, - "text_config": {} # Do not pass in font_size here + "text_config": {} # Do not pass in font_size here } def __init__(self, number: float | complex = 0, **kwargs): @@ -38,17 +38,20 @@ class DecimalNumber(VMobject): def set_submobjects_from_number(self, number: float | complex) -> None: self.number = number self.set_submobjects([]) - string_to_mob_ = lambda s: self.string_to_mob(s, **self.text_config) + self.text_config["font_size"] = self.get_font_size() num_string = self.get_num_string(number) - self.add(*map(string_to_mob_, num_string)) + self.add(*( + Text(ns, **self.text_config) + for ns in num_string + )) # Add non-numerical bits if self.show_ellipsis: - dots = string_to_mob_("...") + dots = Text("...", **self.text_config) dots.arrange(RIGHT, buff=2 * dots[0].get_width()) self.add(dots) if self.unit is not None: - self.unit_sign = self.string_to_mob(self.unit, SingleStringTex) + self.unit_sign = SingleStringTex(self.unit, font_size=self.get_font_size()) self.add(self.unit_sign) self.arrange( @@ -91,12 +94,7 @@ class DecimalNumber(VMobject): self.data["font_size"] = np.array([self.font_size], dtype=float) def get_font_size(self) -> float: - return self.data["font_size"][0] - - def string_to_mob(self, string: str, mob_class: Type[T] = Text, **kwargs) -> T: - mob = mob_class(string, font_size=1, **kwargs) - mob.scale(self.get_font_size()) - return mob + return int(self.data["font_size"][0]) def get_formatter(self, **kwargs) -> str: """ From ed5a435852d5968947cdafb87849a6891df5f87a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 12:14:49 -0600 Subject: [PATCH 17/58] Default to more highlight layers --- manimlib/mobject/types/vectorized_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index a8a8af7c..02575597 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -1202,7 +1202,7 @@ class VHighlight(VGroup): def __init__( self, vmobject: VMobject, - n_layers: int = 3, + n_layers: int = 5, color_bounds: tuple[ManimColor] = (GREY_C, GREY_E), max_stroke_addition: float = 5.0, ): From 7f94a401a8b8c957333e59e2032295d591f53908 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 12:15:00 -0600 Subject: [PATCH 18/58] Wait on start for presenter mode --- manimlib/scene/scene.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 9ca74002..67b7145f 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -116,6 +116,8 @@ class Scene(object): self.file_writer.begin() self.setup() + if self.presenter_mode: + self.wait() try: self.construct() except (EndSceneEarlyException, KeyboardInterrupt): From a791a82111c5df43758f7edd191c034574e00642 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 19:15:26 -0400 Subject: [PATCH 19/58] Bug fix --- manimlib/scene/scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 67b7145f..71f40e83 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -258,7 +258,7 @@ class Scene(object): if self.skip_animations and not ignore_skipping: return - if self.window.is_closing or self.quit_interaction: + if self.window is not None and (self.window.is_closing or self.quit_interaction): raise EndSceneEarlyException() if self.window: From 354030464ebc3c7621dc896abf42cec83b7e45bd Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 19:15:53 -0400 Subject: [PATCH 20/58] Bug patch Tex sometimes rendering black --- manimlib/mobject/svg/tex_mobject.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/svg/tex_mobject.py b/manimlib/mobject/svg/tex_mobject.py index fb444608..52fabe81 100644 --- a/manimlib/mobject/svg/tex_mobject.py +++ b/manimlib/mobject/svg/tex_mobject.py @@ -34,7 +34,7 @@ class SingleStringTex(SVGMobject): "fill_opacity": 1.0, "stroke_width": 0, "svg_default": { - "color": WHITE, + "fill_color": WHITE, }, "path_string_config": { "should_subdivide_sharp_curves": True, From 6a664ece78a21ed595a6d4749baeefbdd3ee7559 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Thu, 28 Apr 2022 19:16:11 -0400 Subject: [PATCH 21/58] Rename InteractiveScene.colors -> InteractiveScene.palette_colors --- manimlib/scene/interactive_scene.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index c585db4b..2bfabae7 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -66,7 +66,7 @@ class InteractiveScene(Scene): ) selection_rectangle_stroke_color = WHITE selection_rectangle_stroke_width = 1.0 - colors = MANIM_COLORS + palette_colors = MANIM_COLORS selection_nudge_size = 0.05 cursor_location_config = dict( font_size=14, @@ -144,7 +144,7 @@ class InteractiveScene(Scene): def get_color_palette(self): palette = VGroup(*( Square(fill_color=color, fill_opacity=1, side_length=1) - for color in self.colors + for color in self.palette_colors )) palette.set_stroke(width=0) palette.arrange(RIGHT, buff=0.5) From 33ffd4863aaa7ecf950b7044181a8e8e3c643698 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 1 May 2022 15:31:07 -0400 Subject: [PATCH 22/58] Add crosshair --- manimlib/scene/interactive_scene.py | 33 ++++++++++++++++++++++++++--- 1 file changed, 30 insertions(+), 3 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 2bfabae7..6cce7dcf 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -2,12 +2,14 @@ import itertools as it import numpy as np import pyperclip -from manimlib.animation.fading import FadeIn +from manimlib.animation.fading import FadeIn, FadeOut from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR from manimlib.constants import FRAME_WIDTH, SMALL_BUFF -from manimlib.constants import MANIM_COLORS, WHITE, GREY_C +from manimlib.constants import PI +from manimlib.constants import MANIM_COLORS, WHITE, GREY_A, GREY_C +from manimlib.mobject.geometry import Line from manimlib.mobject.geometry import Rectangle from manimlib.mobject.geometry import Square from manimlib.mobject.mobject import Group @@ -33,6 +35,7 @@ GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY] RESIZE_KEY = 't' COLOR_KEY = 'c' CURSOR_LOCATION_KEY = 'l' +CURSOR_KEY = 'k' # Note, a lot of the functionality here is still buggy and very much a work in progress. @@ -73,17 +76,21 @@ class InteractiveScene(Scene): fill_color=GREY_C, num_decimal_places=3, ) + crosshair_width = 0.2 + crosshair_color = GREY_A def setup(self): self.selection = Group() self.selection_highlight = self.get_selection_highlight() self.selection_rectangle = self.get_selection_rectangle() - self.color_palette = self.get_color_palette() + self.crosshair = self.get_crosshair() self.cursor_location_label = self.get_cursor_location_label() + self.color_palette = self.get_color_palette() self.unselectables = [ self.selection, self.selection_highlight, self.selection_rectangle, + self.crosshair, self.cursor_location_label, self.camera.frame ] @@ -92,6 +99,7 @@ class InteractiveScene(Scene): self.is_selecting = False self.is_grabbing = False + self.add(self.selection_highlight) def get_selection_rectangle(self): @@ -141,6 +149,18 @@ class InteractiveScene(Scene): except ValueError: pass + def get_crosshair(self): + line = Line(LEFT, RIGHT) + line.insert_n_curves(1) + lines = line.replicate(2) + lines[1].rotate(PI / 2) + crosshair = VMobject() + crosshair.set_points([*lines[0].get_points(), *lines[1].get_points()]) + crosshair.set_width(self.crosshair_width) + crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2, 2, 0, 2]) + crosshair.time_since_movement = 10 + return crosshair + def get_color_palette(self): palette = VGroup(*( Square(fill_color=color, fill_opacity=1, side_length=1) @@ -426,6 +446,9 @@ class InteractiveScene(Scene): vect=[LEFT, UP, RIGHT, DOWN][ARROW_SYMBOLS.index(symbol)], large=(modifiers & SHIFT_MODIFIER), ) + # Adding crosshair + if char in [SELECT_KEY, CURSOR_KEY]: + self.add(self.crosshair) # Conditions for saving state if char in [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY, RESIZE_KEY]: @@ -441,6 +464,9 @@ class InteractiveScene(Scene): self.remove(self.cursor_location_label) elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)): self.prepare_resizing(about_corner=False) + # Removing crosshair + if chr(symbol) in [SELECT_KEY, CURSOR_KEY]: + self.play(FadeOut(self.crosshair, run_time=0.25)) # Mouse actions def handle_grabbing(self, point: np.ndarray): @@ -495,6 +521,7 @@ class InteractiveScene(Scene): def on_mouse_motion(self, point: np.ndarray, d_point: np.ndarray) -> None: super().on_mouse_motion(point, d_point) + self.crosshair.move_to(point) if self.is_grabbing: self.handle_grabbing(point) elif self.window.is_key_pressed(ord(RESIZE_KEY)): From 602fbd1a9f487a08650fd85a354038f98d7c8e0b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 1 May 2022 15:31:31 -0400 Subject: [PATCH 23/58] Fix -e for first line of scene --- manimlib/config.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/manimlib/config.py b/manimlib/config.py index 48c3a186..34c3039c 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -230,6 +230,8 @@ def insert_embed_line(file_name: str, scene_name: str, line_marker: str): if len(line.strip()) > 0 and get_indent(line) < n_spaces: prev_line_num = index - 2 break + if prev_line_num is None: + prev_line_num = len(lines) - 2 elif line_marker.isdigit(): # Treat the argument as a line number prev_line_num = int(line_marker) - 1 @@ -238,7 +240,7 @@ def insert_embed_line(file_name: str, scene_name: str, line_marker: str): try: prev_line_num = next( i - for i in range(len(lines) - 1, scene_line_number, -1) + for i in range(scene_line_number, len(lines) - 1) if line_marker in lines[i] ) except StopIteration: From 4a8e8e5447c5496d0db5cd7934a71badfcf3ce8d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sun, 1 May 2022 15:31:42 -0400 Subject: [PATCH 24/58] Clear later checkpoints --- manimlib/scene/scene.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 71f40e83..a1c7af76 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -754,6 +754,11 @@ class Scene(object): if key not in self.checkpoint_states: log.error(f"No checkpoint at {key}") return + all_keys = list(self.checkpoint_states.keys()) + index = all_keys.index(key) + 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): From 308aadcec539bb13aae18263bf66984e07f76b35 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:09:09 -0700 Subject: [PATCH 25/58] Make show_animation_progress effective --- manimlib/extract_scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index a3686ebc..f961c5fe 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -64,6 +64,7 @@ def get_scene_config(config): "start_at_animation_number", "end_at_animation_number", "leave_progress_bars", + "show_animation_progress", "preview", "presenter_mode", ] From feab79c260498fd7757a304e24c617a4e51ba1df Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:10:57 -0700 Subject: [PATCH 26/58] Get rid of overly complicated anims_from_play_args (which means old style method building is no longer supported) --- manimlib/scene/scene.py | 81 ++++++----------------------------------- 1 file changed, 12 insertions(+), 69 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a1c7af76..a0c5d947 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -21,6 +21,7 @@ from manimlib.constants import SHIFT_MODIFIER from manimlib.event_handler import EVENT_DISPATCHER from manimlib.event_handler.event_type import EventType from manimlib.logger import log +from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import Group from manimlib.mobject.mobject import Mobject from manimlib.mobject.mobject import Point @@ -502,74 +503,16 @@ class Scene(object): kw["override_skip_animations"] = True return self.get_time_progression(duration, **kw) - def anims_from_play_args(self, *args, **kwargs) -> list[Animation]: - """ - Each arg can either be an animation, or a mobject method - followed by that methods arguments (and potentially follow - by a dict of kwargs for that method). - This animation list is built by going through the args list, - and each animation is simply added, but when a mobject method - s hit, a MoveToTarget animation is built using the args that - follow up until either another animation is hit, another method - is hit, or the args list runs out. - """ - animations = [] - state = { - "curr_method": None, - "last_method": None, - "method_args": [], - } - - def compile_method(state): - if state["curr_method"] is None: - return - mobject = state["curr_method"].__self__ - if state["last_method"] and state["last_method"].__self__ is mobject: - animations.pop() - # method should already have target then. - else: - mobject.generate_target() - # - if len(state["method_args"]) > 0 and isinstance(state["method_args"][-1], dict): - method_kwargs = state["method_args"].pop() - else: - method_kwargs = {} - state["curr_method"].__func__( - mobject.target, - *state["method_args"], - **method_kwargs - ) - animations.append(MoveToTarget(mobject)) - state["last_method"] = state["curr_method"] - state["curr_method"] = None - state["method_args"] = [] - - for arg in args: - if inspect.ismethod(arg): - compile_method(state) - state["curr_method"] = arg - elif state["curr_method"] is not None: - state["method_args"].append(arg) - elif isinstance(arg, Mobject): - raise Exception(""" - I think you may have invoked a method - you meant to pass in as a Scene.play argument - """) - else: - try: - anim = prepare_animation(arg) - except TypeError: - raise TypeError(f"Unexpected argument {arg} passed to Scene.play()") - - compile_method(state) - animations.append(anim) - compile_method(state) - - for animation in animations: + def prepare_animations( + self, + proto_animations: list[Animation | _AnimationBuilder], + animation_config: dict, + ): + animations = list(map(prepare_animation, proto_animations)) + for anim in animations: # This is where kwargs to play like run_time and rate_func # get applied to all animations - animation.update_config(**kwargs) - + anim.update_config(**animation_config) return animations def handle_play_like_call(func): @@ -635,11 +578,11 @@ class Scene(object): self.update_mobjects(0) @handle_play_like_call - def play(self, *args, **kwargs) -> None: - if len(args) == 0: + def play(self, *proto_animations, **animation_config) -> None: + if len(proto_animations) == 0: log.warning("Called Scene.play with no animations") return - animations = self.anims_from_play_args(*args, **kwargs) + animations = self.prepare_animations(proto_animations, animation_config) self.begin_animations(animations) self.progress_through_animations(animations) self.finish_animations(animations) From 75e1cff5792065aa1c7fb3eb02e6ee0fa0e8e18d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:11:18 -0700 Subject: [PATCH 27/58] Reorganize how scene state is managed --- manimlib/scene/scene.py | 86 +++++++++++++++++++++++++++-------------- 1 file changed, 58 insertions(+), 28 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a0c5d947..a0ffd1e4 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -1,5 +1,6 @@ from __future__ import annotations +from collections import OrderedDict from functools import wraps import inspect import os @@ -646,52 +647,37 @@ class Scene(object): # Helpers for interactive development - def get_state(self) -> tuple[list[tuple[Mobject, Mobject]], int]: - if self.undo_stack: - last_state = dict(self.undo_stack[-1]) - else: - last_state = {} - result = [] - n_changes = 0 - for mob in self.mobjects: - # If it hasn't changed since the last state, just point to the - # same copy as before - if mob in last_state and last_state[mob].looks_identical(mob): - result.append((mob, last_state[mob])) - else: - result.append((mob, mob.copy())) - n_changes += 1 - return result, n_changes + def get_state(self) -> SceneState: + return SceneState(self) - def restore_state(self, mobject_states: list[tuple[Mobject, Mobject]]): - self.mobjects = [mob.become(mob_copy) for mob, mob_copy in mobject_states] + def restore_state(self, scene_state: SceneState): + scene_state.restore_scene(self) def save_state(self) -> None: if not self.preview: return + state = self.get_state() + if self.undo_stack and state.mobjects_match(self.undo_stack[-1]): + return self.redo_stack = [] - state, n_changes = self.get_state() - if n_changes > 0: - self.undo_stack.append(state) - if len(self.undo_stack) > self.max_num_saved_states: - self.undo_stack.pop(0) + self.undo_stack.append(state) + if len(self.undo_stack) > self.max_num_saved_states: + self.undo_stack.pop(0) def undo(self): if self.undo_stack: - state, n_changes = self.get_state() - self.redo_stack.append(state) + self.redo_stack.append(self.get_state()) self.restore_state(self.undo_stack.pop()) self.refresh_static_mobjects() def redo(self): if self.redo_stack: - state, n_changes = self.get_state() - self.undo_stack.append(state) + self.undo_stack.append(self.get_state()) self.restore_state(self.redo_stack.pop()) self.refresh_static_mobjects() def checkpoint(self, key: str): - self.checkpoint_states[key] = self.get_state()[0] + self.checkpoint_states[key] = self.get_state() def revert_to_checkpoint(self, key: str): if key not in self.checkpoint_states: @@ -858,5 +844,49 @@ class Scene(object): pass +class SceneState(): + def __init__(self, scene: Scene, ignore: list[Mobject] | None = None): + self.time = scene.time + self.num_plays = scene.num_plays + self.mobjects_to_copies = OrderedDict.fromkeys(scene.mobjects) + if ignore: + for mob in ignore: + self.mobjects_to_copies.pop(mob, None) + + last_m2c = scene.undo_stack[-1].mobjects_to_copies if scene.undo_stack else dict() + for mob in self.mobjects_to_copies: + # If it hasn't changed since the last state, just point to the + # same copy as before + if mob in last_m2c and last_m2c[mob].looks_identical(mob): + self.mobjects_to_copies[mob] = last_m2c[mob] + else: + self.mobjects_to_copies[mob] = mob.copy() + + def __eq__(self, state: SceneState): + return all(( + self.time == state.time, + self.num_plays == state.num_plays, + self.mobjects_to_copies == state.mobjects_to_copies + )) + + def mobjects_match(self, state: SceneState): + return self.mobjects_to_copies == state.mobjects_to_copies + + def n_changes(self, state: SceneState): + m2c = state.mobjects_to_copies + return sum( + 1 - int(mob in m2c and mob.looks_identical(m2c[mob])) + for mob in self.mobjects_to_copies + ) + + def restore_scene(self, scene: Scene): + scene.time = self.time + scene.num_plays = self.num_plays + scene.mobjects = [ + mob.become(mob_copy) + for mob, mob_copy in self.mobjects_to_copies.items() + ] + + class EndSceneEarlyException(Exception): pass From c0192100151bcc766334e5436520bc16d5170572 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:12:04 -0700 Subject: [PATCH 28/58] Have InteractiveScene ignore state of crosshair and selection_highlight --- manimlib/scene/interactive_scene.py | 27 +++++++++++++++++++++------ 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 6cce7dcf..2f5a8145 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -22,6 +22,7 @@ from manimlib.mobject.types.vectorized_mobject import VGroup from manimlib.mobject.types.vectorized_mobject import VHighlight from manimlib.mobject.types.vectorized_mobject import VMobject from manimlib.scene.scene import Scene +from manimlib.scene.scene import SceneState from manimlib.utils.family_ops import extract_mobject_family_members from manimlib.utils.space_ops import get_norm from manimlib.utils.tex_file_writing import LatexError @@ -190,6 +191,26 @@ class InteractiveScene(Scene): decimals.add_updater(update_coords) return decimals + # Overrides + def get_state(self): + return SceneState(self, ignore=[ + self.selection_highlight, + self.selection_rectangle, + self.crosshair, + ]) + + def restore_state(self, scene_state: SceneState): + super().restore_state(scene_state) + self.mobjects.insert(0, self.selection_highlight) + + def add(self, *mobjects: Mobject): + super().add(*mobjects) + self.regenerate_selection_search_set() + + def remove(self, *mobjects: Mobject): + super().remove(*mobjects) + self.regenerate_selection_search_set() + # Related to selection def toggle_selection_mode(self): @@ -280,12 +301,6 @@ class InteractiveScene(Scene): self.selection.set_submobjects([]) self.refresh_static_mobjects() - def add(self, *mobjects: Mobject): - super().add(*mobjects) - self.regenerate_selection_search_set() - - def remove(self, *mobjects: Mobject): - super().remove(*mobjects) self.regenerate_selection_search_set() def disable_interaction(self, *mobjects: Mobject): From a09c4402819eaaf4f5bd5c4f4f4cd6d5a21237ce Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:13:05 -0700 Subject: [PATCH 29/58] Slight tweaks to crosshair --- manimlib/scene/interactive_scene.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 2f5a8145..0f9ad886 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -159,7 +159,7 @@ class InteractiveScene(Scene): crosshair.set_points([*lines[0].get_points(), *lines[1].get_points()]) crosshair.set_width(self.crosshair_width) crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2, 2, 0, 2]) - crosshair.time_since_movement = 10 + crosshair.set_animating_status(True) return crosshair def get_color_palette(self): @@ -301,8 +301,6 @@ class InteractiveScene(Scene): self.selection.set_submobjects([]) self.refresh_static_mobjects() - self.regenerate_selection_search_set() - def disable_interaction(self, *mobjects: Mobject): for mob in mobjects: for sm in mob.get_family(): @@ -481,7 +479,7 @@ class InteractiveScene(Scene): self.prepare_resizing(about_corner=False) # Removing crosshair if chr(symbol) in [SELECT_KEY, CURSOR_KEY]: - self.play(FadeOut(self.crosshair, run_time=0.25)) + self.remove(self.crosshair) # Mouse actions def handle_grabbing(self, point: np.ndarray): From 57b7af3bf1402bc0b682e93eecefc5ec159f2d0b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:13:18 -0700 Subject: [PATCH 30/58] Remove old-style method building from ExampleScenes --- example_scenes.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/example_scenes.py b/example_scenes.py index 6806147f..a871915f 100644 --- a/example_scenes.py +++ b/example_scenes.py @@ -77,10 +77,6 @@ class AnimatingMethods(Scene): # ".animate" syntax: self.play(grid.animate.shift(LEFT)) - # Alternatively, you can use the older syntax by passing the - # method and then the arguments to the scene's "play" function: - self.play(grid.shift, LEFT) - # Both of those will interpolate between the mobject's initial # state and whatever happens when you apply that method. # For this example, calling grid.shift(LEFT) would shift the From fddb0b29e1047bd85fac212afdbd7f69bbef1efc Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:38:58 -0700 Subject: [PATCH 31/58] Remove unnecessary import --- manimlib/scene/scene.py | 1 - 1 file changed, 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a0ffd1e4..fb765c72 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -13,7 +13,6 @@ import numpy as np from tqdm import tqdm as ProgressDisplay from manimlib.animation.animation import prepare_animation -from manimlib.animation.transform import MoveToTarget from manimlib.camera.camera import Camera from manimlib.constants import ARROW_SYMBOLS from manimlib.constants import DEFAULT_WAIT_TIME From a6fcfa3b4053b7f68f7b029eae87dbd207d97ad2 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 2 May 2022 11:40:42 -0700 Subject: [PATCH 32/58] Add time_span option to Animation --- manimlib/animation/animation.py | 10 ++++++++++ manimlib/scene/scene.py | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/manimlib/animation/animation.py b/manimlib/animation/animation.py index 03959329..c066f6d6 100644 --- a/manimlib/animation/animation.py +++ b/manimlib/animation/animation.py @@ -6,6 +6,7 @@ from manimlib.mobject.mobject import _AnimationBuilder from manimlib.mobject.mobject import Mobject from manimlib.utils.config_ops import digest_config from manimlib.utils.rate_functions import smooth +from manimlib.utils.rate_functions import squish_rate_func from manimlib.utils.simple_functions import clip from typing import TYPE_CHECKING @@ -23,6 +24,7 @@ DEFAULT_ANIMATION_LAG_RATIO = 0 class Animation(object): CONFIG = { "run_time": DEFAULT_ANIMATION_RUN_TIME, + "time_span": None, # Tuple of times, between which the animation will run "rate_func": smooth, "name": None, # Does this animation add or remove a mobject form the screen @@ -53,6 +55,12 @@ class Animation(object): # played. As much initialization as possible, # especially any mobject copying, should live in # this method + if self.time_span is not None: + start, end = self.time_span + self.run_time = max(end, self.run_time) + self.rate_func = squish_rate_func( + self.rate_func, start / self.run_time, end / self.run_time, + ) self.mobject.set_animating_status(True) self.starting_mobject = self.create_starting_mobject() if self.suspend_mobject_updating: @@ -166,6 +174,8 @@ class Animation(object): return self def get_run_time(self) -> float: + if self.time_span: + return max(self.run_time, self.time_span[1]) return self.run_time def set_rate_func(self, rate_func: Callable[[float], float]): diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index fb765c72..04070537 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -479,7 +479,7 @@ class Scene(object): return times def get_run_time(self, animations: Iterable[Animation]) -> float: - return np.max([animation.run_time for animation in animations]) + return np.max([animation.get_run_time() for animation in animations]) def get_animation_time_progression( self, From a87d3b5f59a64ce5a89ce6e17310bdbf62166157 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 3 May 2022 12:40:43 -0700 Subject: [PATCH 33/58] Add Mobject.arrange_to_fit_dim --- manimlib/mobject/mobject.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index 513f185b..ca992fe3 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -476,6 +476,31 @@ class Mobject(object): self.center() return self + def arrange_to_fit_dim(self, length: float, dim: int, about_edge=ORIGIN): + ref_point = self.get_bounding_box_point(about_edge) + n_submobs = len(self.submobjects) + if n_submobs <= 1: + return + total_length = sum(sm.length_over_dim(dim) for sm in self.submobjects) + buff = (length - total_length) / (n_submobs - 1) + vect = np.zeros(self.dim) + vect[dim] = 1 + x = 0 + for submob in self.submobjects: + submob.set_coord(x, dim, -vect) + x += submob.length_over_dim(dim) + buff + self.move_to(ref_point, about_edge) + return self + + def arrange_to_fit_width(self, width: float): + return self.arrange_to_fit_dim(width, 0) + + def arrange_to_fit_height(self, height: float): + return self.arrange_to_fit_dim(height, 1) + + def arrange_to_fit_depth(self, depth: float): + return self.arrange_to_fit_dim(depth, 2) + def sort( self, point_to_num_func: Callable[[np.ndarray], float] = lambda p: p[0], From 22c5e79f5f66f8fc6ca7022bb0c1d26f720149c1 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 3 May 2022 12:41:44 -0700 Subject: [PATCH 34/58] Some cleanups to the way scenes end --- manimlib/scene/scene.py | 59 ++++++++++++++++++++++------------------- 1 file changed, 32 insertions(+), 27 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 04070537..5e02fe4c 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -64,7 +64,6 @@ class Scene(object): "preview": True, "presenter_mode": False, "show_animation_progress": False, - "linger_after_completion": True, "pan_sensitivity": 3, "max_num_saved_states": 50, } @@ -117,12 +116,14 @@ class Scene(object): self.file_writer.begin() self.setup() - if self.presenter_mode: - self.wait() try: self.construct() - except (EndSceneEarlyException, KeyboardInterrupt): - self.linger_after_completion = False + self.interact() + except EndScene: + pass + except KeyboardInterrupt: + # Get rid keyboard interupt symbols + print("", end="\r") self.tear_down() def setup(self) -> None: @@ -142,35 +143,33 @@ class Scene(object): self.stop_skipping() self.file_writer.finish() if self.window: - if self.linger_after_completion: - self.interact() - else: - self.window.destroy() - self.window = None + self.window.destroy() + self.window = None + if self.inside_embed: + self.embed_shell.enable_gui(None) + self.embed_shell.exiter() 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 log.info( "Tips: You are now in the interactive mode. Now you can use the keyboard" " and the mouse to interact with the scene. Just press `command + q` or `esc`" " if you want to quit." ) + self.skip_animations = False self.refresh_static_mobjects() - try: - while True: - self.update_frame(1 / self.camera.frame_rate) - except (EndSceneEarlyException, KeyboardInterrupt): - return + while not self.is_window_closing(): + self.update_frame(1 / self.camera.frame_rate) def embed(self, close_scene_on_exit: bool = True) -> None: if not self.preview: - # Ignore embed calls when there is no preview - return + return # Embed is only relevant with a preview self.inside_embed = True self.stop_skipping() - self.linger_after_completion = False self.update_frame() self.save_state() @@ -214,8 +213,10 @@ class Scene(object): # Enables gui interactions during the embed def inputhook(context): while not context.input_is_ready(): - if not self.window.is_closing: + if not self.is_window_closing(): self.update_frame(dt=0) + if self.is_window_closing(): + shell.ask_exit() pt_inputhooks.register("manim", inputhook) shell.enable_gui("manim") @@ -226,7 +227,10 @@ class Scene(object): # That comes up a fair bit during scene construction, to get around this, # we (admittedly sketchily) update the global namespace to match the local # namespace, since this is just a shell session anyway. - shell.events.register("pre_run_cell", lambda: shell.user_global_ns.update(shell.user_ns)) + shell.events.register( + "pre_run_cell", + lambda: shell.user_global_ns.update(shell.user_ns) + ) # Operation to run after each ipython command def post_cell_func(): @@ -236,13 +240,11 @@ class Scene(object): shell.events.register("post_run_cell", post_cell_func) - # Launch shell, with stack_depth=2 indicating we should use caller globals/locals shell(local_ns=local_ns, stack_depth=2) - self.inside_embed = False # End scene when exiting an embed if close_scene_on_exit: - raise EndSceneEarlyException() + raise EndScene() # Only these methods should touch the camera @@ -259,8 +261,8 @@ class Scene(object): if self.skip_animations and not ignore_skipping: return - if self.window is not None and (self.window.is_closing or self.quit_interaction): - raise EndSceneEarlyException() + if self.is_window_closing(): + raise EndScene() if self.window: self.window.clear() @@ -444,7 +446,7 @@ class Scene(object): self.stop_skipping() if self.end_at_animation_number is not None: if self.num_plays >= self.end_at_animation_number: - raise EndSceneEarlyException() + raise EndScene() def stop_skipping(self) -> None: self.virtual_animation_start_time = self.time @@ -707,6 +709,9 @@ class Scene(object): path = os.path.join(directory, file_name) return Mobject.load(path) + def is_window_closing(self): + return self.window and (self.window.is_closing or self.quit_interaction) + # Event handling def on_mouse_motion( @@ -887,5 +892,5 @@ class SceneState(): ] -class EndSceneEarlyException(Exception): +class EndScene(Exception): pass From a9a151d4eff80cc37b9db0fe7117727aac45ba09 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 3 May 2022 12:45:16 -0700 Subject: [PATCH 35/58] Have presenter mode hold before first play call --- manimlib/scene/scene.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 5e02fe4c..dc97dfa7 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -145,9 +145,6 @@ class Scene(object): if self.window: self.window.destroy() self.window = None - if self.inside_embed: - self.embed_shell.enable_gui(None) - self.embed_shell.exiter() def interact(self) -> None: # If there is a window, enter a loop @@ -522,6 +519,8 @@ class Scene(object): def wrapper(self, *args, **kwargs): if self.inside_embed: self.save_state() + if self.presenter_mode and self.num_plays == 0: + self.hold_loop() self.update_skipping_status() should_write = not self.skip_animations @@ -601,9 +600,7 @@ class Scene(object): if self.presenter_mode and not self.skip_animations and not ignore_presenter_mode: if note: log.info(note) - while self.hold_on_wait: - self.update_frame(dt=1 / self.camera.frame_rate) - self.hold_on_wait = True + self.hold_loop() else: time_progression = self.get_wait_time_progression(duration, stop_condition) last_t = 0 @@ -617,6 +614,11 @@ class Scene(object): self.refresh_static_mobjects() return self + def hold_loop(self): + while self.hold_on_wait: + self.update_frame(dt=1 / self.camera.frame_rate) + self.hold_on_wait = True + def wait_until( self, stop_condition: Callable[[], bool], From 181038e2f3cfc65c2928df84609b7428947e0756 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 4 May 2022 09:30:54 -0700 Subject: [PATCH 36/58] More curves to cross by default --- manimlib/mobject/shape_matchers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/mobject/shape_matchers.py b/manimlib/mobject/shape_matchers.py index 78cc8100..bd799aae 100644 --- a/manimlib/mobject/shape_matchers.py +++ b/manimlib/mobject/shape_matchers.py @@ -87,7 +87,7 @@ class Cross(VGroup): Line(UL, DR), Line(UR, DL), ) - self.insert_n_curves(2) + self.insert_n_curves(20) self.replace(mobject, stretch=True) self.set_stroke(self.stroke_color, width=self.stroke_width) From c4d452248ac74a30c1d9d68ad531780302860249 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 4 May 2022 21:21:49 -0700 Subject: [PATCH 37/58] Move mirror output path logic to config.py --- manimlib/config.py | 13 +++++++++++-- manimlib/scene/scene_file_writer.py | 16 ++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index 34c3039c..5a446ca2 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -337,6 +337,16 @@ def get_configuration(args): else: file_ext = ".mp4" + dir_config = custom_config["directories"] + output_directory = args.video_dir or dir_config["output"] + if dir_config["mirror_module_path"]: + to_cut = dir_config["removed_mirror_prefix"] + input_file = os.path.abspath(args.file) + output_directory = os.path.join( + output_directory, + input_file.replace(to_cut, "").replace(".py", "") + ) + file_writer_config = { "write_to_movie": not args.skip_animations and write_file, "break_into_partial_movies": custom_config["break_into_partial_movies"], @@ -345,8 +355,7 @@ def get_configuration(args): # If -t is passed in (for transparent), this will be RGBA "png_mode": "RGBA" if args.transparent else "RGB", "movie_file_extension": file_ext, - "mirror_module_path": custom_config["directories"]["mirror_module_path"], - "output_directory": args.video_dir or custom_config["directories"]["output"], + "output_directory": output_directory, "file_name": args.file_name, "input_file_path": args.file or "", "open_file_upon_completion": args.open, diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index 3a647fe9..fffbd788 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -37,9 +37,6 @@ class SceneFileWriter(object): "png_mode": "RGBA", "save_last_frame": False, "movie_file_extension": ".mp4", - # Should the path of output files mirror the directory - # structure of the module holding the scene? - "mirror_module_path": False, # What python file is generating this scene "input_file_path": "", # Where should this be written @@ -63,10 +60,6 @@ class SceneFileWriter(object): # Output directories and files def init_output_directories(self) -> None: out_dir = self.output_directory or "" - if self.mirror_module_path: - module_dir = self.get_default_module_directory() - out_dir = os.path.join(out_dir, module_dir) - scene_name = self.file_name or self.get_default_scene_name() if self.save_last_frame: image_dir = guarantee_existence(os.path.join(out_dir, "images")) @@ -81,7 +74,9 @@ class SceneFileWriter(object): movie_dir, "partial_movie_files", scene_name, )) # A place to save mobjects - self.saved_mobject_directory = os.path.join(out_dir, "mobjects") + self.saved_mobject_directory = os.path.join( + out_dir, "mobjects", str(self.scene) + ) def get_default_module_directory(self) -> str: path, _ = os.path.splitext(self.input_file_path) @@ -124,10 +119,7 @@ class SceneFileWriter(object): return self.movie_file_path def get_saved_mobject_directory(self) -> str: - return guarantee_existence(os.path.join( - self.saved_mobject_directory, - str(self.scene), - )) + return guarantee_existence(self.saved_mobject_directory) def get_saved_mobject_path(self, mobject: Mobject) -> str | None: directory = self.get_saved_mobject_directory() From d6629715595ec0e65e91c05f985c11d74255cc32 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 4 May 2022 21:22:00 -0700 Subject: [PATCH 38/58] Larger cursor location label --- manimlib/scene/interactive_scene.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 0f9ad886..f357605e 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -73,7 +73,7 @@ class InteractiveScene(Scene): palette_colors = MANIM_COLORS selection_nudge_size = 0.05 cursor_location_config = dict( - font_size=14, + font_size=24, fill_color=GREY_C, num_decimal_places=3, ) From 93fc81ac9d055330d274da9fcb1731a1bbba62c9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 4 May 2022 21:22:22 -0700 Subject: [PATCH 39/58] Add show_progress option to checkpoint_paste --- manimlib/scene/scene.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index dc97dfa7..cdc1d062 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -190,7 +190,7 @@ class Scene(object): # As long as the copied selection starts with a comment, # this will revert to the state of the scene at the first # point of running. - def checkpoint_paste(skip=False): + def checkpoint_paste(skip=False, show_progress=False): pasted = pyperclip.paste() line0 = pasted.lstrip().split("\n")[0] if line0.startswith("#"): @@ -201,9 +201,14 @@ class Scene(object): if skip: originally_skip = self.skip_animations self.skip_animations = True + if show_progress: + originally_show_animation_progress = self.show_animation_progress + self.show_animation_progress = True shell.run_line_magic("paste", "") if skip: self.skip_animations = originally_skip + if show_progress: + self.show_animation_progress = originally_show_animation_progress local_ns['checkpoint_paste'] = checkpoint_paste From 41b811a5e7c03f528d41555217106e62b287ca3b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 4 May 2022 21:22:48 -0700 Subject: [PATCH 40/58] Update frame on all play calls when skipping animations, so as to provide a rapid preview during scene loading --- manimlib/scene/scene.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index cdc1d062..a217414f 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -545,6 +545,10 @@ class Scene(object): if self.inside_embed: self.save_state() + if self.skip_animations and self.window is not None: + # Show some quick frames along the way + self.update_frame(dt=0, ignore_skipping=True) + self.num_plays += 1 return wrapper From 584e259b443b04462a4e80998c5a831e849fa063 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:44:51 -0700 Subject: [PATCH 41/58] Bug fix for mirrored directories --- manimlib/config.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index 5a446ca2..46c58632 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -339,13 +339,13 @@ def get_configuration(args): dir_config = custom_config["directories"] output_directory = args.video_dir or dir_config["output"] - if dir_config["mirror_module_path"]: + if dir_config["mirror_module_path"] and args.file: to_cut = dir_config["removed_mirror_prefix"] - input_file = os.path.abspath(args.file) - output_directory = os.path.join( - output_directory, - input_file.replace(to_cut, "").replace(".py", "") - ) + ext = os.path.abspath(args.file) + ext = ext.replace(to_cut, "").replace(".py", "") + if ext.startswith("_"): + ext = ext[1:] + output_directory = os.path.join(output_directory, ext) file_writer_config = { "write_to_movie": not args.skip_animations and write_file, From cd866573b52dfeeca310579967d952f1dcd8ee9b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:45:06 -0700 Subject: [PATCH 42/58] Add about_edge argument to arrange_to_fit_width, etc. --- manimlib/mobject/mobject.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/manimlib/mobject/mobject.py b/manimlib/mobject/mobject.py index ca992fe3..d55ae77a 100644 --- a/manimlib/mobject/mobject.py +++ b/manimlib/mobject/mobject.py @@ -492,14 +492,14 @@ class Mobject(object): self.move_to(ref_point, about_edge) return self - def arrange_to_fit_width(self, width: float): - return self.arrange_to_fit_dim(width, 0) + def arrange_to_fit_width(self, width: float, about_edge=ORIGIN): + return self.arrange_to_fit_dim(width, 0, about_edge) - def arrange_to_fit_height(self, height: float): - return self.arrange_to_fit_dim(height, 1) + def arrange_to_fit_height(self, height: float, about_edge=ORIGIN): + return self.arrange_to_fit_dim(height, 1, about_edge) - def arrange_to_fit_depth(self, depth: float): - return self.arrange_to_fit_dim(depth, 2) + def arrange_to_fit_depth(self, depth: float, about_edge=ORIGIN): + return self.arrange_to_fit_dim(depth, 2, about_edge) def sort( self, From 48689c8c7bc0029bf5c1b540c11f647e857d419b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:45:27 -0700 Subject: [PATCH 43/58] Add DecimalNumber.get_tex --- manimlib/mobject/numbers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index b51c4983..c7387d6d 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -132,6 +132,9 @@ class DecimalNumber(VMobject): "i" ]) + def get_tex(self): + return self.num_string + def set_value(self, number: float | complex): move_to_point = self.get_edge_center(self.edge_to_fix) old_submobjects = list(self.submobjects) From 4fdaeb1547d8e226f99a1c140cc18d4e9d9b56f0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:45:51 -0700 Subject: [PATCH 44/58] Fix(?) issue with numbers forgetting style info when resetting value --- manimlib/mobject/numbers.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index c7387d6d..79f05326 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -34,12 +34,13 @@ class DecimalNumber(VMobject): def __init__(self, number: float | complex = 0, **kwargs): super().__init__(**kwargs) self.set_submobjects_from_number(number) + self.init_colors() def set_submobjects_from_number(self, number: float | complex) -> None: self.number = number self.set_submobjects([]) self.text_config["font_size"] = self.get_font_size() - num_string = self.get_num_string(number) + num_string = self.num_string = self.get_num_string(number) self.add(*( Text(ns, **self.text_config) for ns in num_string @@ -137,11 +138,10 @@ class DecimalNumber(VMobject): def set_value(self, number: float | complex): move_to_point = self.get_edge_center(self.edge_to_fix) - old_submobjects = list(self.submobjects) + style = self.family_members_with_points()[0].get_style() self.set_submobjects_from_number(number) self.move_to(move_to_point, self.edge_to_fix) - for sm1, sm2 in zip(self.submobjects, old_submobjects): - sm1.match_style(sm2) + self.set_style(**style) return self def _handle_scale_side_effects(self, scale_factor: float) -> None: From 0060a4860c9d6b073a60cd839269c213446bba7b Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:46:15 -0700 Subject: [PATCH 45/58] Bug fix to VMobject.match_style --- manimlib/mobject/types/vectorized_mobject.py | 58 ++++++++++---------- 1 file changed, 30 insertions(+), 28 deletions(-) diff --git a/manimlib/mobject/types/vectorized_mobject.py b/manimlib/mobject/types/vectorized_mobject.py index 02575597..d58def01 100644 --- a/manimlib/mobject/types/vectorized_mobject.py +++ b/manimlib/mobject/types/vectorized_mobject.py @@ -203,36 +203,38 @@ class VMobject(Mobject): shadow: float | None = None, recurse: bool = True ): - if fill_rgba is not None: - self.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba)) - else: - self.set_fill( - color=fill_color, - opacity=fill_opacity, - recurse=recurse - ) + for mob in self.get_family(recurse): + if fill_rgba is not None: + mob.data['fill_rgba'] = resize_with_interpolation(fill_rgba, len(fill_rgba)) + else: + mob.set_fill( + color=fill_color, + opacity=fill_opacity, + recurse=False + ) - if stroke_rgba is not None: - self.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba)) - self.set_stroke( - width=stroke_width, - background=stroke_background, - ) - else: - self.set_stroke( - color=stroke_color, - width=stroke_width, - opacity=stroke_opacity, - recurse=recurse, - background=stroke_background, - ) + if stroke_rgba is not None: + mob.data['stroke_rgba'] = resize_with_interpolation(stroke_rgba, len(fill_rgba)) + mob.set_stroke( + width=stroke_width, + background=stroke_background, + recurse=False, + ) + else: + mob.set_stroke( + color=stroke_color, + width=stroke_width, + opacity=stroke_opacity, + recurse=False, + background=stroke_background, + ) - if reflectiveness is not None: - self.set_reflectiveness(reflectiveness, recurse=recurse) - if gloss is not None: - self.set_gloss(gloss, recurse=recurse) - if shadow is not None: - self.set_shadow(shadow, recurse=recurse) + if reflectiveness is not None: + mob.set_reflectiveness(reflectiveness, recurse=False) + if gloss is not None: + mob.set_gloss(gloss, recurse=False) + if shadow is not None: + mob.set_shadow(shadow, recurse=False) return self def get_style(self): From cef7c383a5392952977584aca7ee54632587a85a Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:46:56 -0700 Subject: [PATCH 46/58] Add scene time to information that can be displayed with the appropriate key press --- manimlib/scene/interactive_scene.py | 57 ++++++++++++++++++++--------- 1 file changed, 39 insertions(+), 18 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index f357605e..df1d6c5a 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -2,7 +2,7 @@ import itertools as it import numpy as np import pyperclip -from manimlib.animation.fading import FadeIn, FadeOut +from manimlib.animation.fading import FadeIn from manimlib.constants import ARROW_SYMBOLS, CTRL_SYMBOL, DELETE_SYMBOL, SHIFT_SYMBOL from manimlib.constants import COMMAND_MODIFIER, SHIFT_MODIFIER from manimlib.constants import DL, DOWN, DR, LEFT, ORIGIN, RIGHT, UL, UP, UR @@ -35,7 +35,7 @@ Y_GRAB_KEY = 'v' GRAB_KEYS = [GRAB_KEY, X_GRAB_KEY, Y_GRAB_KEY] RESIZE_KEY = 't' COLOR_KEY = 'c' -CURSOR_LOCATION_KEY = 'l' +INFORMATION_KEY = 'i' CURSOR_KEY = 'k' @@ -77,6 +77,11 @@ class InteractiveScene(Scene): fill_color=GREY_C, num_decimal_places=3, ) + time_label_config = dict( + font_size=24, + fill_color=GREY_C, + num_decimal_places=1, + ) crosshair_width = 0.2 crosshair_color = GREY_A @@ -85,14 +90,14 @@ class InteractiveScene(Scene): self.selection_highlight = self.get_selection_highlight() self.selection_rectangle = self.get_selection_rectangle() self.crosshair = self.get_crosshair() - self.cursor_location_label = self.get_cursor_location_label() + self.information_label = self.get_information_label() self.color_palette = self.get_color_palette() self.unselectables = [ self.selection, self.selection_highlight, self.selection_rectangle, self.crosshair, - self.cursor_location_label, + self.information_label, self.camera.frame ] self.select_top_level_mobs = True @@ -160,6 +165,7 @@ class InteractiveScene(Scene): crosshair.set_width(self.crosshair_width) crosshair.set_stroke(self.crosshair_color, width=[2, 0, 2, 2, 0, 2]) crosshair.set_animating_status(True) + crosshair.fix_in_frame() return crosshair def get_color_palette(self): @@ -174,22 +180,28 @@ class InteractiveScene(Scene): palette.fix_in_frame() return palette - def get_cursor_location_label(self): - decimals = VGroup(*( + def get_information_label(self): + loc_label = VGroup(*( DecimalNumber(**self.cursor_location_config) for n in range(3) )) - def update_coords(decimals): - for mob, coord in zip(decimals, self.mouse_point.get_location()): + def update_coords(loc_label): + for mob, coord in zip(loc_label, self.mouse_point.get_location()): mob.set_value(coord) - decimals.arrange(RIGHT, buff=decimals.get_height()) - decimals.to_corner(DR, buff=SMALL_BUFF) - decimals.fix_in_frame() - return decimals + loc_label.arrange(RIGHT, buff=loc_label.get_height()) + loc_label.to_corner(DR, buff=SMALL_BUFF) + loc_label.fix_in_frame() + return loc_label - decimals.add_updater(update_coords) - return decimals + loc_label.add_updater(update_coords) + + time_label = DecimalNumber(0, **self.time_label_config) + time_label.to_corner(DL, buff=SMALL_BUFF) + time_label.fix_in_frame() + time_label.add_updater(lambda m, dt: m.increment_value(dt)) + + return VGroup(loc_label, time_label) # Overrides def get_state(self): @@ -211,6 +223,9 @@ class InteractiveScene(Scene): super().remove(*mobjects) self.regenerate_selection_search_set() + # def increment_time(self, dt: float) -> None: + # super().increment_time(dt) + # Related to selection def toggle_selection_mode(self): @@ -391,6 +406,12 @@ class InteractiveScene(Scene): else: self.remove(self.color_palette) + def display_information(self, show=True): + if show: + self.add(self.information_label) + else: + self.remove(self.information_label) + def group_selection(self): group = self.get_group(*self.selection) self.add(group) @@ -432,8 +453,8 @@ class InteractiveScene(Scene): self.prepare_resizing(about_corner=True) elif char == COLOR_KEY and modifiers == 0: self.toggle_color_palette() - elif char == CURSOR_LOCATION_KEY and modifiers == 0: - self.add(self.cursor_location_label) + elif char == INFORMATION_KEY and modifiers == 0: + self.display_information() elif char == "c" and modifiers == COMMAND_MODIFIER: self.copy_selection() elif char == "v" and modifiers == COMMAND_MODIFIER: @@ -473,8 +494,8 @@ class InteractiveScene(Scene): self.gather_new_selection() if chr(symbol) in GRAB_KEYS: self.is_grabbing = False - elif chr(symbol) == CURSOR_LOCATION_KEY: - self.remove(self.cursor_location_label) + elif chr(symbol) == INFORMATION_KEY: + self.display_information(False) elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)): self.prepare_resizing(about_corner=False) # Removing crosshair From 97643d788d06312b511b29f72a88a8ec477afe62 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:47:21 -0700 Subject: [PATCH 47/58] Default to showing animation progress in embedded runs --- manimlib/scene/scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a217414f..a5085d31 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -191,6 +191,7 @@ class Scene(object): # this will revert to the state of the scene at the first # point of running. def checkpoint_paste(skip=False, show_progress=False): + def checkpoint_paste(skip=False, show_progress=True): pasted = pyperclip.paste() line0 = pasted.lstrip().split("\n")[0] if line0.startswith("#"): From 11d19b6d5702275cd5785c0c0c0b33475199cc7d Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:47:42 -0700 Subject: [PATCH 48/58] Update frame at the end of each checkpoint_paste --- manimlib/scene/scene.py | 1 + 1 file changed, 1 insertion(+) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index a5085d31..b189f606 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -199,6 +199,7 @@ class Scene(object): self.checkpoint(line0) else: self.revert_to_checkpoint(line0) + self.update_frame(dt=0) if skip: originally_skip = self.skip_animations self.skip_animations = True From f741217c34f307ee45752cc13dbccc0c030f1eb9 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Wed, 11 May 2022 12:48:08 -0700 Subject: [PATCH 49/58] Make sure keyboard interrupted renders don't overwrite pre-existing video files --- manimlib/scene/scene.py | 2 +- manimlib/scene/scene_file_writer.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index b189f606..7b29deae 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -124,6 +124,7 @@ class Scene(object): except KeyboardInterrupt: # Get rid keyboard interupt symbols print("", end="\r") + self.file_writer.ended_with_interrupt = True self.tear_down() def setup(self) -> None: @@ -190,7 +191,6 @@ class Scene(object): # As long as the copied selection starts with a comment, # this will revert to the state of the scene at the first # point of running. - def checkpoint_paste(skip=False, show_progress=False): def checkpoint_paste(skip=False, show_progress=True): pasted = pyperclip.paste() line0 = pasted.lstrip().split("\n")[0] diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index fffbd788..d2e211eb 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -54,6 +54,7 @@ class SceneFileWriter(object): self.scene: Scene = scene self.writing_process: sp.Popen | None = None self.has_progress_display: bool = False + self.ended_with_interrupt: bool = False self.init_output_directories() self.init_audio() @@ -297,7 +298,11 @@ class SceneFileWriter(object): self.writing_process.terminate() if self.has_progress_display: self.progress_display.close() - shutil.move(self.temp_file_path, self.final_file_path) + + if not self.ended_with_interrupt: + shutil.move(self.temp_file_path, self.final_file_path) + else: + self.movie_file_path = self.temp_file_path def combine_movie_files(self) -> None: kwargs = { From 5f56778cdf92b2d46b199b7805805ebe9c6d4702 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 May 2022 17:28:11 -0700 Subject: [PATCH 50/58] Don't update frame during window closing on cell execution --- manimlib/scene/scene.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index 7b29deae..d04f8d73 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -239,7 +239,8 @@ class Scene(object): # Operation to run after each ipython command def post_cell_func(): self.refresh_static_mobjects() - self.update_frame(dt=0, ignore_skipping=True) + if not self.is_window_closing(): + self.update_frame(dt=0, ignore_skipping=True) self.save_state() shell.events.register("post_run_cell", post_cell_func) From 25de729bb3e3c2d161b04bebcc4ae982d3cb2f26 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 May 2022 17:28:31 -0700 Subject: [PATCH 51/58] Only lock data for mobjects without updaters --- manimlib/animation/transform.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/manimlib/animation/transform.py b/manimlib/animation/transform.py index 37307a0c..3d69528d 100644 --- a/manimlib/animation/transform.py +++ b/manimlib/animation/transform.py @@ -68,10 +68,11 @@ class Transform(Animation): self.target_copy = self.target_mobject.copy() self.mobject.align_data_and_family(self.target_copy) super().begin() - self.mobject.lock_matching_data( - self.starting_mobject, - self.target_copy, - ) + if not self.mobject.has_updaters: + self.mobject.lock_matching_data( + self.starting_mobject, + self.target_copy, + ) def finish(self) -> None: super().finish() From dd5d239971d6e5a1309ebba7337118145b31f2b0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 May 2022 17:29:07 -0700 Subject: [PATCH 52/58] Change convention for how camera_qualities are represented in default_config.yml --- manimlib/config.py | 22 +++++++++++----------- manimlib/default_config.yml | 21 +++++++-------------- 2 files changed, 18 insertions(+), 25 deletions(-) diff --git a/manimlib/config.py b/manimlib/config.py index 46c58632..b4df5ca6 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -415,31 +415,31 @@ def get_configuration(args): def get_camera_configuration(args, custom_config): camera_config = {} - camera_qualities = get_custom_config()["camera_qualities"] + camera_resolutions = get_custom_config()["camera_resolutions"] if args.low_quality: - quality = camera_qualities["low"] + resolution = camera_resolutions["low"] elif args.medium_quality: - quality = camera_qualities["medium"] + resolution = camera_resolutions["med"] elif args.hd: - quality = camera_qualities["high"] + resolution = camera_resolutions["high"] elif args.uhd: - quality = camera_qualities["ultra_high"] + resolution = camera_resolutions["4k"] else: - quality = camera_qualities[camera_qualities["default_quality"]] + resolution = camera_resolutions[camera_resolutions["default_resolution"]] - if args.resolution: - quality["resolution"] = args.resolution if args.frame_rate: - quality["frame_rate"] = int(args.frame_rate) + frame_rate = int(args.frame_rate) + else: + frame_rate = get_custom_config()["frame_rate"] - width_str, height_str = quality["resolution"].split("x") + width_str, height_str = resolution.split("x") width = int(width_str) height = int(height_str) camera_config.update({ "pixel_width": width, "pixel_height": height, - "frame_rate": quality["frame_rate"], + "frame_rate": frame_rate, }) try: diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index c948a8e5..e73a07bd 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -42,17 +42,10 @@ full_screen: False # easier when working with the broken up scene, which # effectively has cuts at all the places you might want. break_into_partial_movies: False -camera_qualities: - low: - resolution: "854x480" - frame_rate: 15 - medium: - resolution: "1280x720" - frame_rate: 30 - high: - resolution: "1920x1080" - frame_rate: 30 - ultra_high: - resolution: "3840x2160" - frame_rate: 60 - default_quality: "high" +camera_resolutions: + low: "854x480" + med: "1280x720" + high: "1920x1080" + 4k: "3840x2160" + default_resolution: "high" +frame_rate: 30 \ No newline at end of file From fb3cf308dfbf521d1592824bf15749f410b715f0 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 May 2022 17:42:07 -0700 Subject: [PATCH 53/58] Make sure init_customization matches default_config.yml --- manimlib/utils/init_config.py | 29 +++++++++-------------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 2e5d4b32..19815353 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -57,25 +57,14 @@ def init_customization() -> None: "window_monitor": 0, "full_screen": False, "break_into_partial_movies": False, - "camera_qualities": { - "low": { - "resolution": "854x480", - "frame_rate": 15, - }, - "medium": { - "resolution": "1280x720", - "frame_rate": 30, - }, - "high": { - "resolution": "1920x1080", - "frame_rate": 60, - }, - "ultra_high": { - "resolution": "3840x2160", - "frame_rate": 60, - }, - "default_quality": "", - } + "camera_resolutions": { + "low": "854x480", + "medium": "1280x720", + "high": "1920x1080", + "4k": "3840x2160", + "default_resolution": "high", + }, + "frame_rate": 30, } console = Console() @@ -83,7 +72,7 @@ def init_customization() -> None: # print("Initialize configuration") try: scope = Prompt.ask( - " Select the scope of the configuration", + " Select the scope of the configuration", choices=["global", "local"], default="local" ) From 6decb0c32aec21c09007f9a2b91aaa8e642ca848 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Sat, 14 May 2022 17:47:31 -0700 Subject: [PATCH 54/58] Rename frame_rate -> fps --- docs/source/documentation/constants.rst | 2 +- docs/source/getting_started/configuration.rst | 2 +- manimlib/camera/camera.py | 4 ++-- manimlib/config.py | 10 +++++----- manimlib/constants.py | 2 +- manimlib/default_config.yml | 2 +- manimlib/extract_scene.py | 2 +- manimlib/scene/scene.py | 8 ++++---- manimlib/scene/scene_file_writer.py | 6 +++--- manimlib/utils/init_config.py | 4 ++-- 10 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/source/documentation/constants.rst b/docs/source/documentation/constants.rst index 7dabd50e..81ec4937 100644 --- a/docs/source/documentation/constants.rst +++ b/docs/source/documentation/constants.rst @@ -18,7 +18,7 @@ Frame and pixel shape DEFAULT_PIXEL_HEIGHT = 1080 DEFAULT_PIXEL_WIDTH = 1920 - DEFAULT_FRAME_RATE = 30 + DEFAULT_FPS = 30 Buffs ----- diff --git a/docs/source/getting_started/configuration.rst b/docs/source/getting_started/configuration.rst index 9b1a147f..c34948b5 100644 --- a/docs/source/getting_started/configuration.rst +++ b/docs/source/getting_started/configuration.rst @@ -56,7 +56,7 @@ flag abbr function ``--start_at_animation_number START_AT_ANIMATION_NUMBER`` ``-n`` Start rendering not from the first animation, but from another, specified by its index. If you passing two comma separated values, e.g. "3,6", it will end the rendering at the second value. ``--embed LINENO`` ``-e`` Takes a line number as an argument, and results in the scene being called as if the line ``self.embed()`` was inserted into the scene code at that line number ``--resolution RESOLUTION`` ``-r`` Resolution, passed as "WxH", e.g. "1920x1080" -``--frame_rate FRAME_RATE`` Frame rate, as an integer +``--fps FPS`` Frame rate, as an integer ``--color COLOR`` ``-c`` Background color ``--leave_progress_bars`` Leave progress bars displayed in terminal ``--video_dir VIDEO_DIR`` Directory to write video diff --git a/manimlib/camera/camera.py b/manimlib/camera/camera.py index ab1e2d33..2b9ab8ff 100644 --- a/manimlib/camera/camera.py +++ b/manimlib/camera/camera.py @@ -11,7 +11,7 @@ from scipy.spatial.transform import Rotation from manimlib.constants import BLACK from manimlib.constants import DEGREES, RADIANS -from manimlib.constants import DEFAULT_FRAME_RATE +from manimlib.constants import DEFAULT_FPS from manimlib.constants import DEFAULT_PIXEL_HEIGHT, DEFAULT_PIXEL_WIDTH from manimlib.constants import FRAME_HEIGHT, FRAME_WIDTH from manimlib.constants import DOWN, LEFT, ORIGIN, OUT, RIGHT, UP @@ -170,7 +170,7 @@ class Camera(object): "frame_config": {}, "pixel_width": DEFAULT_PIXEL_WIDTH, "pixel_height": DEFAULT_PIXEL_HEIGHT, - "frame_rate": DEFAULT_FRAME_RATE, + "fps": DEFAULT_FPS, # Note: frame height and width will be resized to match # the pixel aspect ratio "background_color": BLACK, diff --git a/manimlib/config.py b/manimlib/config.py index b4df5ca6..cf8fbed0 100644 --- a/manimlib/config.py +++ b/manimlib/config.py @@ -136,7 +136,7 @@ def parse_cli(): help="Resolution, passed as \"WxH\", e.g. \"1920x1080\"", ) parser.add_argument( - "--frame_rate", + "--fps", help="Frame rate, as an integer", ) parser.add_argument( @@ -427,10 +427,10 @@ def get_camera_configuration(args, custom_config): else: resolution = camera_resolutions[camera_resolutions["default_resolution"]] - if args.frame_rate: - frame_rate = int(args.frame_rate) + if args.fps: + fps = int(args.fps) else: - frame_rate = get_custom_config()["frame_rate"] + fps = get_custom_config()["fps"] width_str, height_str = resolution.split("x") width = int(width_str) @@ -439,7 +439,7 @@ def get_camera_configuration(args, custom_config): camera_config.update({ "pixel_width": width, "pixel_height": height, - "frame_rate": frame_rate, + "fps": fps, }) try: diff --git a/manimlib/constants.py b/manimlib/constants.py index f0bc3269..f1303d01 100644 --- a/manimlib/constants.py +++ b/manimlib/constants.py @@ -10,7 +10,7 @@ FRAME_X_RADIUS = FRAME_WIDTH / 2 DEFAULT_PIXEL_HEIGHT = 1080 DEFAULT_PIXEL_WIDTH = 1920 -DEFAULT_FRAME_RATE = 30 +DEFAULT_FPS = 30 SMALL_BUFF = 0.1 MED_SMALL_BUFF = 0.25 diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index e73a07bd..6a1135ec 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -48,4 +48,4 @@ camera_resolutions: high: "1920x1080" 4k: "3840x2160" default_resolution: "high" -frame_rate: 30 \ No newline at end of file +fps: 30 \ No newline at end of file diff --git a/manimlib/extract_scene.py b/manimlib/extract_scene.py index f961c5fe..bc1cde7e 100644 --- a/manimlib/extract_scene.py +++ b/manimlib/extract_scene.py @@ -88,7 +88,7 @@ def compute_total_frames(scene_class, scene_config): pre_scene = scene_class(**pre_config) pre_scene.run() total_time = pre_scene.time - pre_scene.skip_time - return int(total_time * scene_config["camera_config"]["frame_rate"]) + return int(total_time * scene_config["camera_config"]["fps"]) def get_scenes_to_render(scene_classes, scene_config, config): diff --git a/manimlib/scene/scene.py b/manimlib/scene/scene.py index d04f8d73..73ae7d1d 100644 --- a/manimlib/scene/scene.py +++ b/manimlib/scene/scene.py @@ -74,7 +74,7 @@ class Scene(object): from manimlib.window import Window self.window = Window(scene=self, **self.window_config) self.camera_config["ctx"] = self.window.ctx - self.camera_config["frame_rate"] = 30 # Where's that 30 from? + self.camera_config["fps"] = 30 # Where's that 30 from? self.undo_stack = [] self.redo_stack = [] else: @@ -161,7 +161,7 @@ class Scene(object): self.skip_animations = False self.refresh_static_mobjects() while not self.is_window_closing(): - self.update_frame(1 / self.camera.frame_rate) + self.update_frame(1 / self.camera.fps) def embed(self, close_scene_on_exit: bool = True) -> None: if not self.preview: @@ -469,7 +469,7 @@ class Scene(object): if self.skip_animations and not override_skip_animations: return [run_time] - times = np.arange(0, run_time, 1 / self.camera.frame_rate) + times = np.arange(0, run_time, 1 / self.camera.fps) if self.file_writer.has_progress_display: self.file_writer.set_progress_display_subdescription(desc) @@ -628,7 +628,7 @@ class Scene(object): def hold_loop(self): while self.hold_on_wait: - self.update_frame(dt=1 / self.camera.frame_rate) + self.update_frame(dt=1 / self.camera.fps) self.hold_on_wait = True def wait_until( diff --git a/manimlib/scene/scene_file_writer.py b/manimlib/scene/scene_file_writer.py index d2e211eb..33a8e610 100644 --- a/manimlib/scene/scene_file_writer.py +++ b/manimlib/scene/scene_file_writer.py @@ -97,9 +97,9 @@ class SceneFileWriter(object): def get_resolution_directory(self) -> str: pixel_height = self.scene.camera.pixel_height - frame_rate = self.scene.camera.frame_rate + fps = self.scene.camera.fps return "{}p{}".format( - pixel_height, frame_rate + pixel_height, fps ) # Directory getters @@ -234,7 +234,7 @@ class SceneFileWriter(object): self.final_file_path = file_path self.temp_file_path = stem + "_temp" + ext - fps = self.scene.camera.frame_rate + fps = self.scene.camera.fps width, height = self.scene.camera.get_pixel_shape() command = [ diff --git a/manimlib/utils/init_config.py b/manimlib/utils/init_config.py index 19815353..a2786ca0 100644 --- a/manimlib/utils/init_config.py +++ b/manimlib/utils/init_config.py @@ -9,7 +9,7 @@ from rich import box from rich.console import Console from rich.prompt import Confirm from rich.prompt import Prompt -from rich.rule import Rule +from rich.rule import Rule from rich.table import Table from typing import TYPE_CHECKING @@ -64,7 +64,7 @@ def init_customization() -> None: "4k": "3840x2160", "default_resolution": "high", }, - "frame_rate": 30, + "fps": 30, } console = Console() From ad58a9e6c145913dd7d1537964e212b34aab8ce3 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 23 May 2022 11:07:39 -0700 Subject: [PATCH 55/58] Tiny refactor --- manimlib/mobject/numbers.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/manimlib/mobject/numbers.py b/manimlib/mobject/numbers.py index 79f05326..1181d191 100644 --- a/manimlib/mobject/numbers.py +++ b/manimlib/mobject/numbers.py @@ -116,13 +116,14 @@ class DecimalNumber(VMobject): ] ]) config.update(kwargs) + ndp = config["num_decimal_places"] return "".join([ "{", config.get("field_name", ""), ":", "+" if config["include_sign"] else "", "," if config["group_with_commas"] else "", - ".", str(config["num_decimal_places"]), "f", + f".{ndp}f", "}", ]) From ca523c8a5ee0b3f5b77c5540fbcbc9c4f5343759 Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 23 May 2022 11:07:51 -0700 Subject: [PATCH 56/58] Fix bubble direction --- manimlib/mobject/svg/drawings.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/manimlib/mobject/svg/drawings.py b/manimlib/mobject/svg/drawings.py index ed2102c3..b8d58d21 100644 --- a/manimlib/mobject/svg/drawings.py +++ b/manimlib/mobject/svg/drawings.py @@ -308,7 +308,7 @@ class Bubble(SVGMobject): } def __init__(self, **kwargs): - digest_config(self, kwargs, locals()) + digest_config(self, kwargs) if self.file_name is None: raise Exception("Must invoke Bubble subclass") SVGMobject.__init__(self, self.file_name, **kwargs) @@ -317,7 +317,11 @@ class Bubble(SVGMobject): self.stretch_to_fit_width(self.width) if self.direction[0] > 0: self.flip() - self.direction_was_specified = ("direction" in kwargs) + if "direction" in kwargs: + self.direction = kwargs["direction"] + self.direction_was_specified = True + else: + self.direction_was_specified = False self.content = Mobject() self.refresh_triangulation() From 117a34dc67650778d9b266688b4540adaf25962c Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Mon, 23 May 2022 11:08:08 -0700 Subject: [PATCH 57/58] Change crosshair behavior --- manimlib/scene/interactive_scene.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/manimlib/scene/interactive_scene.py b/manimlib/scene/interactive_scene.py index 8adea511..044bc39c 100644 --- a/manimlib/scene/interactive_scene.py +++ b/manimlib/scene/interactive_scene.py @@ -290,6 +290,8 @@ class InteractiveScene(Scene): ) result.add_updater(lambda m: m.replace(mobject, stretch=True)) return result + elif isinstance(mobject, DotCloud): + return Mobject() else: return self.get_corner_dots(mobject) @@ -483,7 +485,12 @@ class InteractiveScene(Scene): large=(modifiers & SHIFT_MODIFIER), ) # Adding crosshair - if char in [SELECT_KEY, CURSOR_KEY]: + if char == CURSOR_KEY: + if self.crosshair in self.mobjects: + self.remove(self.crosshair) + else: + self.add(self.crosshair) + if char == SELECT_KEY: self.add(self.crosshair) # Conditions for saving state @@ -494,15 +501,13 @@ class InteractiveScene(Scene): super().on_key_release(symbol, modifiers) if chr(symbol) == SELECT_KEY: self.gather_new_selection() + # self.remove(self.crosshair) if chr(symbol) in GRAB_KEYS: self.is_grabbing = False elif chr(symbol) == INFORMATION_KEY: self.display_information(False) elif symbol == SHIFT_SYMBOL and self.window.is_key_pressed(ord(RESIZE_KEY)): self.prepare_resizing(about_corner=False) - # Removing crosshair - if chr(symbol) in [SELECT_KEY, CURSOR_KEY]: - self.remove(self.crosshair) # Mouse actions def handle_grabbing(self, point: np.ndarray): From 83b4aa6b88b6c3defb19f204189681f5afbb219e Mon Sep 17 00:00:00 2001 From: Grant Sanderson Date: Tue, 24 May 2022 15:16:59 -0700 Subject: [PATCH 58/58] Let defaullt text alignment be decided in default_config --- manimlib/default_config.yml | 1 + manimlib/mobject/svg/text_mobject.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/manimlib/default_config.yml b/manimlib/default_config.yml index 6a1135ec..261eec56 100644 --- a/manimlib/default_config.yml +++ b/manimlib/default_config.yml @@ -28,6 +28,7 @@ tex: universal_import_line: "from manimlib import *" style: font: "Consolas" + text_alignment: "LEFT" background_color: "#333333" # Set the position of preview window, you can use directions, e.g. UL/DR/OL/OO/... # also, you can also specify the position(pixel) of the upper left corner of diff --git a/manimlib/mobject/svg/text_mobject.py b/manimlib/mobject/svg/text_mobject.py index 93623c31..b5613614 100644 --- a/manimlib/mobject/svg/text_mobject.py +++ b/manimlib/mobject/svg/text_mobject.py @@ -68,7 +68,7 @@ class MarkupText(StringMobject): "lsh": None, "justify": False, "indent": 0, - "alignment": "LEFT", + "alignment": "", "line_width": None, "font": "", "slant": NORMAL, @@ -114,6 +114,8 @@ class MarkupText(StringMobject): if not self.font: self.font = get_customization()["style"]["font"] + if not self.alignment: + self.alignment = get_customization()["style"]["text_alignment"] if self.is_markup: self.validate_markup_string(text)