From 6a96e2a03761bd889c8ee73b060262582da137de Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Mon, 5 Oct 2020 03:02:39 -0700 Subject: [PATCH 01/25] Remove Scene decorators and consolidate play() and wait() --- manim/scene/js_scene.py | 1 - manim/scene/scene.py | 231 +++++++++++++--------------------------- 2 files changed, 74 insertions(+), 158 deletions(-) diff --git a/manim/scene/js_scene.py b/manim/scene/js_scene.py index 80aa682d7f..8a3bf89c78 100644 --- a/manim/scene/js_scene.py +++ b/manim/scene/js_scene.py @@ -109,7 +109,6 @@ def progress_through_animations(self): logger.error(e) self.animation_finished.wait() - @scene.handle_play_like_call def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): self.update_mobjects(dt=0) # Any problems with this? self.animations = [] diff --git a/manim/scene/scene.py b/manim/scene/scene.py index f6608c8c19..c7a2392fec 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -25,39 +25,6 @@ from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call -def handle_play_like_call(func): - """ - This method is used internally to wrap the - passed function, into a function that - actually writes to the video stream. - Simultaneously, it also adds to the number - of animations played. - - Parameters - ---------- - func : function - The play() like function that has to be - written to the video file stream. - - Returns - ------- - function - The play() like function that can now write - to the video file stream. - """ - - def wrapper(self, *args, **kwargs): - allow_write = not file_writer_config["skip_animations"] - if not self.camera.use_js_renderer: - self.file_writer.begin_animation(allow_write) - func(self, *args, **kwargs) - if not self.camera.use_js_renderer: - self.file_writer.end_animation(allow_write) - self.num_plays += 1 - - return wrapper - - class Scene(Container): """A Scene is the canvas of your animation. @@ -832,82 +799,6 @@ def update_skipping_status(self): file_writer_config["skip_animations"] = True raise EndSceneEarlyException() - def handle_caching_play(func): - """ - Decorator that returns a wrapped version of func that will compute the hash of the play invocation. - - The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. - - Parameters - ---------- - func : Callable[[...], None] - The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`. - """ - - def wrapper(self, *args, **kwargs): - self.revert_to_original_skipping_status() - self.update_skipping_status() - animations = self.compile_play_args_to_animation_list(*args, **kwargs) - self.add_mobjects_from_animations(animations) - if file_writer_config["skip_animations"]: - logger.debug(f"Skipping animation {self.num_plays}") - func(self, *args, **kwargs) - return - if not file_writer_config["disable_caching"]: - mobjects_on_scene = self.get_mobjects() - hash_play = get_hash_from_play_call( - self, self.camera, animations, mobjects_on_scene - ) - self.play_hashes_list.append(hash_play) - if self.file_writer.is_already_cached(hash_play): - logger.info( - f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", - {"hash_play": hash_play}, - ) - file_writer_config["skip_animations"] = True - else: - hash_play = "uncached_{:05}".format(self.num_plays) - self.play_hashes_list.append(hash_play) - func(self, *args, **kwargs) - - return wrapper - - def handle_caching_wait(func): - """ - Decorator that returns a wrapped version of func that will compute the hash of the wait invocation. - - The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. - - Parameters - ---------- - func : Callable[[...], None] - The wait like function that has to be written to the video file stream. Take the same parameters as `scene.wait`. - """ - - def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.revert_to_original_skipping_status() - self.update_skipping_status() - if file_writer_config["skip_animations"]: - logger.debug(f"Skipping wait {self.num_plays}") - func(self, duration, stop_condition) - return - if not file_writer_config["disable_caching"]: - hash_wait = get_hash_from_wait_call( - self, self.camera, duration, stop_condition, self.get_mobjects() - ) - self.play_hashes_list.append(hash_wait) - if self.file_writer.is_already_cached(hash_wait): - logger.info( - f"Wait {self.num_plays} : Using cached data (hash : {hash_wait})" - ) - file_writer_config["skip_animations"] = True - else: - hash_wait = "uncached_{:05}".format(self.num_plays) - self.play_hashes_list.append(hash_wait) - func(self, duration, stop_condition) - - return wrapper - def begin_animations(self, animations): """ This method begins the list of animations that is passed, @@ -972,9 +863,54 @@ def finish_animations(self, animations): else: self.update_mobjects(0) - @handle_caching_play - @handle_play_like_call + def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): + self.play(duration=duration, stop_condition=stop_condition) + def play(self, *args, **kwargs): + self.cached_play(*args, **kwargs) + self.num_plays += 1 + + def cached_play(self, *args, **kwargs): + self.revert_to_original_skipping_status() + self.update_skipping_status() + animations = self.compile_play_args_to_animation_list(*args, **kwargs) + self.add_mobjects_from_animations(animations) + if file_writer_config["skip_animations"]: + logger.debug(f"Skipping animation {self.num_plays}") + self.file_writer_wrapped_play(*args, **kwargs) + return + if not file_writer_config["disable_caching"]: + mobjects_on_scene = self.get_mobjects() + hash_play = get_hash_from_play_call( + self, self.camera, animations, mobjects_on_scene + ) + self.play_hashes_list.append(hash_play) + if self.file_writer.is_already_cached(hash_play): + logger.info( + f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", + {"hash_play": hash_play}, + ) + file_writer_config["skip_animations"] = True + else: + hash_play = "uncached_{:05}".format(self.num_plays) + self.play_hashes_list.append(hash_play) + self.file_writer_wrapped_play(*args, **kwargs) + + def file_writer_wrapped_play(self, *args, **kwargs): + allow_write = not file_writer_config["skip_animations"] + self.file_writer.begin_animation(allow_write) + + self.play_or_wait(*args, **kwargs) + + self.file_writer.end_animation(allow_write) + + def play_or_wait(self, *args, **kwargs): + if "duration" in kwargs: + self.wait_internal(**kwargs) + else: + self.play_internal(*args, **kwargs) + + def play_internal(self, *args, **kwargs): """ This method is used to prep the animations for rendering, apply the arguments and parameters required to them, @@ -1003,6 +939,33 @@ def play(self, *args, **kwargs): self.finish_animations(self.animations) + def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): + self.update_mobjects(dt=0) # Any problems with this? + self.animations = [] + self.duration = duration + self.stop_condition = stop_condition + self.last_t = 0 + + if self.should_update_mobjects(): + time_progression = self.get_wait_time_progression(duration, stop_condition) + # TODO, be smart about setting a static image + # the same way Scene.play does + for t in time_progression: + self.update_animation_to_time(t) + self.update_frame() + self.add_frame(self.get_frame()) + if stop_condition is not None and stop_condition(): + time_progression.close() + break + elif self.skip_animations: + # Do nothing + return self + else: + self.update_frame() + dt = 1 / self.camera.frame_rate + self.add_frame(self.get_frame(), num_frames=int(duration / dt)) + return self + def clean_up_animations(self, *animations): """ This method cleans up and removes from the @@ -1073,52 +1036,6 @@ def get_wait_time_progression(self, duration, stop_condition): time_progression.set_description("Waiting {}".format(self.num_plays)) return time_progression - @handle_caching_wait - @handle_play_like_call - def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - """ - This method is used to wait, and do nothing to the scene, for some - duration. - Updaters stop updating, nothing happens. - - Parameters - ---------- - duration : float or int, optional - The duration of wait time. - stop_condition : - A function that determines whether to stop waiting or not. - - Returns - ------- - Scene - The scene, after waiting. - """ - self.update_mobjects(dt=0) # Any problems with this? - self.animations = [] - self.duration = duration - self.stop_condition = stop_condition - self.last_t = 0 - - if self.should_update_mobjects(): - time_progression = self.get_wait_time_progression(duration, stop_condition) - # TODO, be smart about setting a static image - # the same way Scene.play does - for t in time_progression: - self.update_animation_to_time(t) - self.update_frame() - self.add_frame(self.get_frame()) - if stop_condition is not None and stop_condition(): - time_progression.close() - break - elif self.skip_animations: - # Do nothing - return self - else: - self.update_frame() - dt = 1 / self.camera.frame_rate - self.add_frame(self.get_frame(), num_frames=int(duration / dt)) - return self - def wait_until(self, stop_condition, max_time=60): """ Like a wrapper for wait(). From e42c48ffdd634cbbe9a30528df63bdea2b697233 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Mon, 5 Oct 2020 03:34:12 -0700 Subject: [PATCH 02/25] Add CairoRenderer, move Scene.get_frame() --- manim/grpc/impl/frame_server_impl.py | 2 +- manim/renderer/cairo_renderer.py | 20 +++++++++++++++++ manim/scene/scene.py | 33 ++++++++++------------------ tests/helpers/graphical_units.py | 2 +- tests/utils/GraphicalUnitTester.py | 2 +- 5 files changed, 34 insertions(+), 25 deletions(-) create mode 100644 manim/renderer/cairo_renderer.py diff --git a/manim/grpc/impl/frame_server_impl.py b/manim/grpc/impl/frame_server_impl.py index e288142cc7..8f0f2eac02 100644 --- a/manim/grpc/impl/frame_server_impl.py +++ b/manim/grpc/impl/frame_server_impl.py @@ -98,7 +98,7 @@ def GetFrameAtTime(self, request, context): selected_scene.static_image, ) serialized_mobject_list, duration = selected_scene.add_frame( - selected_scene.get_frame() + selected_scene.renderer.get_frame() ) resp = list_to_frame_response( selected_scene, duration, serialized_mobject_list diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py new file mode 100644 index 0000000000..7887f2739b --- /dev/null +++ b/manim/renderer/cairo_renderer.py @@ -0,0 +1,20 @@ +import numpy as np + + +class CairoRenderer: + def __init__(self, scene, camera, file_writer): + self.scene = scene + self.camera = camera + self.file_writer = file_writer + + def get_frame(self): + """ + Gets the current frame as NumPy array. + + Returns + ------- + np.array + NumPy array of pixel values of each pixel in screen. + The shape of the array is height x width x 3 + """ + return np.array(self.camera.pixel_array) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index c7a2392fec..374e021e09 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -23,6 +23,7 @@ from ..scene.scene_file_writer import SceneFileWriter from ..utils.iterables import list_update from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call +from ..renderer.cairo_renderer import CairoRenderer class Scene(Container): @@ -65,11 +66,11 @@ def construct(self): def __init__(self, **kwargs): Container.__init__(self, **kwargs) self.camera = self.camera_class(**camera_config) - if not self.camera.use_js_renderer: - self.file_writer = SceneFileWriter( - self, - **file_writer_config, - ) + self.file_writer = SceneFileWriter( + self, + **file_writer_config, + ) + self.renderer = CairoRenderer(self, self.camera, self.file_writer) self.play_hashes_list = [] self.mobjects = [] @@ -167,18 +168,6 @@ def get_attrs(self, *keys): """ return [getattr(self, key) for key in keys] - def get_frame(self): - """ - Gets the current frame as NumPy array. - - Returns - ------- - np.array - NumPy array of pixel values of each pixel in screen. - The shape of the array is height x width x 3 - """ - return np.array(self.camera.pixel_array) - def update_frame( # TODO Description in Docstring self, mobjects=None, @@ -221,7 +210,7 @@ def update_frame( # TODO Description in Docstring def freeze_background(self): self.update_frame() - self.camera = Camera(self.get_frame()) + self.camera = Camera(self.renderer.get_frame()) self.clear() ### @@ -823,7 +812,7 @@ def progress_through_animations(self): for t in self.get_animation_time_progression(self.animations): self.update_animation_to_time(t) self.update_frame(self.moving_mobjects, self.static_image) - self.add_frame(self.get_frame()) + self.add_frame(self.renderer.get_frame()) def update_animation_to_time(self, t): """ @@ -931,7 +920,7 @@ def play_internal(self, *args, **kwargs): # have to be rendered every frame self.moving_mobjects = self.get_moving_mobjects(*self.animations) self.update_frame(excluded_mobjects=self.moving_mobjects) - self.static_image = self.get_frame() + self.static_image = self.renderer.get_frame() self.last_t = 0 self.run_time = self.get_run_time(self.animations) @@ -953,7 +942,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): for t in time_progression: self.update_animation_to_time(t) self.update_frame() - self.add_frame(self.get_frame()) + self.add_frame(self.renderer.get_frame()) if stop_condition is not None and stop_condition(): time_progression.close() break @@ -963,7 +952,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): else: self.update_frame() dt = 1 / self.camera.frame_rate - self.add_frame(self.get_frame(), num_frames=int(duration / dt)) + self.add_frame(self.renderer.get_frame(), num_frames=int(duration / dt)) return self def clean_up_animations(self, *animations): diff --git a/tests/helpers/graphical_units.py b/tests/helpers/graphical_units.py index ad8914a032..94880e92e9 100644 --- a/tests/helpers/graphical_units.py +++ b/tests/helpers/graphical_units.py @@ -40,7 +40,7 @@ def set_test_scene(scene_object, module_name): file_writer_config["text_dir"] = os.path.join(tmpdir, "text") file_writer_config["tex_dir"] = os.path.join(tmpdir, "tex") scene = scene_object() - data = scene.get_frame() + data = scene.renderer.get_frame() tests_directory = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) path_control_data = os.path.join( diff --git a/tests/utils/GraphicalUnitTester.py b/tests/utils/GraphicalUnitTester.py index 7f856ded1c..698f1a0bb0 100644 --- a/tests/utils/GraphicalUnitTester.py +++ b/tests/utils/GraphicalUnitTester.py @@ -128,7 +128,7 @@ def _show_diff_helper(self, frame_data, expected_frame_data): def test(self, show_diff=False): """Compare pre-rendered frame to the frame rendered during the test.""" - frame_data = self.scene.get_frame() + frame_data = self.scene.renderer.get_frame() expected_frame_data = self._load_data() assert frame_data.shape == expected_frame_data.shape, ( From aef7eec50f62d4f53e10c2b4db54fd459c8ef7ac Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Mon, 5 Oct 2020 03:47:25 -0700 Subject: [PATCH 03/25] Move Scene.update_frame() --- manim/renderer/cairo_renderer.py | 42 ++++++++++++++++++++++++++ manim/scene/scene.py | 52 ++++---------------------------- 2 files changed, 48 insertions(+), 46 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 7887f2739b..71a7fd7ffa 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,4 +1,6 @@ import numpy as np +from .. import file_writer_config +from ..utils.iterables import list_update class CairoRenderer: @@ -7,6 +9,46 @@ def __init__(self, scene, camera, file_writer): self.camera = camera self.file_writer = file_writer + def update_frame( # TODO Description in Docstring + self, + mobjects=None, + background=None, + include_submobjects=True, + ignore_skipping=True, + **kwargs, + ): + """Update the frame. + + Parameters + ---------- + mobjects: list, optional + list of mobjects + + background: np.ndarray, optional + Pixel Array for Background. + + include_submobjects: bool, optional + + ignore_skipping : bool, optional + + **kwargs + + """ + if file_writer_config["skip_animations"] and not ignore_skipping: + return + if mobjects is None: + mobjects = list_update( + self.scene.mobjects, + self.scene.foreground_mobjects, + ) + if background is not None: + self.camera.set_frame_to_background(background) + else: + self.camera.reset() + + kwargs["include_submobjects"] = include_submobjects + self.camera.capture_mobjects(mobjects, **kwargs) + def get_frame(self): """ Gets the current frame as NumPy array. diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 374e021e09..6f4733ff25 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -168,48 +168,8 @@ def get_attrs(self, *keys): """ return [getattr(self, key) for key in keys] - def update_frame( # TODO Description in Docstring - self, - mobjects=None, - background=None, - include_submobjects=True, - ignore_skipping=True, - **kwargs, - ): - """Update the frame. - - Parameters - ---------- - mobjects: list, optional - list of mobjects - - background: np.ndarray, optional - Pixel Array for Background. - - include_submobjects: bool, optional - - ignore_skipping : bool, optional - - **kwargs - - """ - if file_writer_config["skip_animations"] and not ignore_skipping: - return - if mobjects is None: - mobjects = list_update( - self.mobjects, - self.foreground_mobjects, - ) - if background is not None: - self.camera.set_frame_to_background(background) - else: - self.camera.reset() - - kwargs["include_submobjects"] = include_submobjects - self.camera.capture_mobjects(mobjects, **kwargs) - def freeze_background(self): - self.update_frame() + self.renderer.update_frame() self.camera = Camera(self.renderer.get_frame()) self.clear() @@ -811,7 +771,7 @@ def progress_through_animations(self): """ for t in self.get_animation_time_progression(self.animations): self.update_animation_to_time(t) - self.update_frame(self.moving_mobjects, self.static_image) + self.renderer.update_frame(self.moving_mobjects, self.static_image) self.add_frame(self.renderer.get_frame()) def update_animation_to_time(self, t): @@ -919,7 +879,7 @@ def play_internal(self, *args, **kwargs): # Paint all non-moving objects onto the screen, so they don't # have to be rendered every frame self.moving_mobjects = self.get_moving_mobjects(*self.animations) - self.update_frame(excluded_mobjects=self.moving_mobjects) + self.renderer.update_frame(excluded_mobjects=self.moving_mobjects) self.static_image = self.renderer.get_frame() self.last_t = 0 self.run_time = self.get_run_time(self.animations) @@ -941,7 +901,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): # the same way Scene.play does for t in time_progression: self.update_animation_to_time(t) - self.update_frame() + self.renderer.update_frame() self.add_frame(self.renderer.get_frame()) if stop_condition is not None and stop_condition(): time_progression.close() @@ -950,7 +910,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): # Do nothing return self else: - self.update_frame() + self.renderer.update_frame() dt = 1 / self.camera.frame_rate self.add_frame(self.renderer.get_frame(), num_frames=int(duration / dt)) return self @@ -1116,7 +1076,7 @@ def show_frame(self): Opens the current frame in the Default Image Viewer of your system. """ - self.update_frame(ignore_skipping=True) + self.renderer.update_frame(ignore_skipping=True) self.camera.get_image().show() From b1f8f6bab09d3c13d9908498b8b76ba6fbf3d0cc Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Mon, 5 Oct 2020 04:36:26 -0700 Subject: [PATCH 04/25] Move Scene.freeze_background() and Camera.extract_mobject_family_members() --- manim/camera/camera.py | 36 ++++++------------------------- manim/scene/scene.py | 14 ++++++------ manim/scene/vector_space_scene.py | 5 ++++- manim/utils/family.py | 32 +++++++++++++++++++++++++++ 4 files changed, 49 insertions(+), 38 deletions(-) create mode 100644 manim/utils/family.py diff --git a/manim/camera/camera.py b/manim/camera/camera.py index ef2aa55eac..e2aa8948e7 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -28,6 +28,7 @@ from ..utils.simple_functions import fdiv from ..utils.space_ops import angle_of_vector from ..utils.space_ops import get_norm +from ..utils.family import extract_mobject_family_members class Camera(object): @@ -363,33 +364,6 @@ def set_frame_to_background(self, background): #### - # TODO, it's weird that this is part of camera. - # Clearly it should live elsewhere. - def extract_mobject_family_members(self, mobjects, only_those_with_points=False): - """Returns a list of the types of mobjects and - their family members present. - - Parameters - ---------- - mobjects : Mobject - The Mobjects currently in the Scene - only_those_with_points : bool, optional - Whether or not to only do this for - those mobjects that have points. By default False - - Returns - ------- - list - list of the mobjects and family members. - """ - if only_those_with_points: - method = Mobject.family_members_with_points - else: - method = Mobject.get_family - if self.use_z_index: - mobjects = sorted(mobjects, key=lambda m: m.z_index) - return remove_list_redundancies(list(it.chain(*[method(m) for m in mobjects]))) - def get_mobjects_to_display( self, mobjects, include_submobjects=True, excluded_mobjects=None ): @@ -411,11 +385,13 @@ def get_mobjects_to_display( list of mobjects """ if include_submobjects: - mobjects = self.extract_mobject_family_members( - mobjects, only_those_with_points=True + mobjects = extract_mobject_family_members( + mobjects, use_z_index=self.use_z_index, only_those_with_points=True ) if excluded_mobjects: - all_excluded = self.extract_mobject_family_members(excluded_mobjects) + all_excluded = extract_mobject_family_members( + excluded_mobjects, use_z_index=self.use_z_index + ) mobjects = list_difference_update(mobjects, all_excluded) return mobjects diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 6f4733ff25..cf612d8483 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -23,6 +23,7 @@ from ..scene.scene_file_writer import SceneFileWriter from ..utils.iterables import list_update from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call +from ..utils.family import extract_mobject_family_members from ..renderer.cairo_renderer import CairoRenderer @@ -168,11 +169,6 @@ def get_attrs(self, *keys): """ return [getattr(self, key) for key in keys] - def freeze_background(self): - self.renderer.update_frame() - self.camera = Camera(self.renderer.get_frame()) - self.clear() - ### def update_mobjects(self, dt): @@ -248,7 +244,9 @@ def get_mobject_family_members(self): list List of mobject family members. """ - return self.camera.extract_mobject_family_members(self.mobjects) + return extract_mobject_family_members( + self.mobjects, use_z_index=self.renderer.camera.use_z_index + ) def add(self, *mobjects): """ @@ -337,7 +335,9 @@ def restructure_mobjects( The Scene mobject with restructured Mobjects. """ if extract_families: - to_remove = self.camera.extract_mobject_family_members(to_remove) + to_remove = extract_mobject_family_members( + to_remove, use_z_index=self.renderer.camera.use_z_index + ) _list = getattr(self, mobject_list_name) new_list = self.get_restructured_mobject_list(_list, to_remove) setattr(self, mobject_list_name, new_list) diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index 5d0f187201..c7b02491b0 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -109,7 +109,10 @@ def lock_in_faded_grid(self, dimness=0.7, axes_dimness=0.5): axes.set_color(WHITE) axes.fade(axes_dimness) self.add(axes) - self.freeze_background() + + self.renderer.update_frame() + self.renderer.camera = Camera(self.renderer.get_frame()) + self.clear() def get_vector(self, numerical_vector, **kwargs): """ diff --git a/manim/utils/family.py b/manim/utils/family.py new file mode 100644 index 0000000000..b7f8498b93 --- /dev/null +++ b/manim/utils/family.py @@ -0,0 +1,32 @@ +import itertools as it + +from ..mobject.mobject import Mobject +from ..utils.iterables import remove_list_redundancies + + +def extract_mobject_family_members( + mobjects, use_z_index=False, only_those_with_points=False +): + """Returns a list of the types of mobjects and + their family members present. + + Parameters + ---------- + mobjects : Mobject + The Mobjects currently in the Scene + only_those_with_points : bool, optional + Whether or not to only do this for + those mobjects that have points. By default False + + Returns + ------- + list + list of the mobjects and family members. + """ + if only_those_with_points: + method = Mobject.family_members_with_points + else: + method = Mobject.get_family + if use_z_index: + mobjects = sorted(mobjects, key=lambda m: m.z_index) + return remove_list_redundancies(list(it.chain(*[method(m) for m in mobjects]))) From 2555bc7b6ac3c927105dc5981a23832aafca5cb6 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Tue, 6 Oct 2020 02:27:20 -0700 Subject: [PATCH 05/25] Move Scene.update_skipping_status() --- manim/renderer/cairo_renderer.py | 17 +++++++++++++++++ manim/scene/scene.py | 25 +++---------------------- manim/utils/exceptions.py | 2 ++ 3 files changed, 22 insertions(+), 22 deletions(-) create mode 100644 manim/utils/exceptions.py diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 71a7fd7ffa..96c0fbb0ea 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,6 +1,7 @@ import numpy as np from .. import file_writer_config from ..utils.iterables import list_update +from ..utils.exceptions import EndSceneEarlyException class CairoRenderer: @@ -60,3 +61,19 @@ def get_frame(self): The shape of the array is height x width x 3 """ return np.array(self.camera.pixel_array) + + def update_skipping_status(self): + """ + This method is used internally to check if the current + animation needs to be skipped or not. It also checks if + the number of animations that were played correspond to + the number of animations that need to be played, and + raises an EndSceneEarlyException if they don't correspond. + """ + if file_writer_config["from_animation_number"]: + if self.scene.num_plays < file_writer_config["from_animation_number"]: + file_writer_config["skip_animations"] = True + if file_writer_config["upto_animation_number"]: + if self.scene.num_plays > file_writer_config["upto_animation_number"]: + file_writer_config["skip_animations"] = True + raise EndSceneEarlyException() diff --git a/manim/scene/scene.py b/manim/scene/scene.py index cf612d8483..73264db56f 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -1,7 +1,7 @@ """Basic canvas for animations.""" -__all__ = ["Scene", "EndSceneEarlyException"] +__all__ = ["Scene"] import inspect @@ -25,6 +25,7 @@ from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call from ..utils.family import extract_mobject_family_members from ..renderer.cairo_renderer import CairoRenderer +from ..utils.exceptions import EndSceneEarlyException class Scene(Container): @@ -732,22 +733,6 @@ def compile_method(state): return animations - def update_skipping_status(self): - """ - This method is used internally to check if the current - animation needs to be skipped or not. It also checks if - the number of animations that were played correspond to - the number of animations that need to be played, and - raises an EndSceneEarlyException if they don't correspond. - """ - if file_writer_config["from_animation_number"]: - if self.num_plays < file_writer_config["from_animation_number"]: - file_writer_config["skip_animations"] = True - if file_writer_config["upto_animation_number"]: - if self.num_plays > file_writer_config["upto_animation_number"]: - file_writer_config["skip_animations"] = True - raise EndSceneEarlyException() - def begin_animations(self, animations): """ This method begins the list of animations that is passed, @@ -821,7 +806,7 @@ def play(self, *args, **kwargs): def cached_play(self, *args, **kwargs): self.revert_to_original_skipping_status() - self.update_skipping_status() + self.renderer.update_skipping_status() animations = self.compile_play_args_to_animation_list(*args, **kwargs) self.add_mobjects_from_animations(animations) if file_writer_config["skip_animations"]: @@ -1078,7 +1063,3 @@ def show_frame(self): """ self.renderer.update_frame(ignore_skipping=True) self.camera.get_image().show() - - -class EndSceneEarlyException(Exception): - pass diff --git a/manim/utils/exceptions.py b/manim/utils/exceptions.py new file mode 100644 index 0000000000..0326f46df6 --- /dev/null +++ b/manim/utils/exceptions.py @@ -0,0 +1,2 @@ +class EndSceneEarlyException(Exception): + pass From 0fd90747ed4b0eb8ec3fd0d4d2e8a82e86dac814 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Tue, 6 Oct 2020 02:50:56 -0700 Subject: [PATCH 06/25] Remove Scene.get_mobjects_from_last_animation() --- manim/scene/scene.py | 17 +---------------- manim/scene/vector_space_scene.py | 2 +- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 73264db56f..14f40bac06 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -790,6 +790,7 @@ def finish_animations(self, animations): for animation in animations: animation.finish() animation.clean_up_from_scene(self) + # TODO: This is only used in one place and should probably be removed. self.mobjects_from_last_animation = [anim.mobject for anim in animations] if file_writer_config["skip_animations"]: # TODO, run this call in for each animation? @@ -921,22 +922,6 @@ def clean_up_animations(self, *animations): animation.clean_up_from_scene(self) return self - def get_mobjects_from_last_animation(self): - """ - This method returns the mobjects from the previous - played animation, if any exist, and returns an empty - list if not. - - Returns - -------- - list - The list of mobjects from the previous animation. - - """ - if hasattr(self, "mobjects_from_last_animation"): - return self.mobjects_from_last_animation - return [] - def get_wait_time_progression(self, duration, stop_condition): """ This method is used internally to obtain the CommandLine diff --git a/manim/scene/vector_space_scene.py b/manim/scene/vector_space_scene.py index c7b02491b0..6e59ae5ca0 100644 --- a/manim/scene/vector_space_scene.py +++ b/manim/scene/vector_space_scene.py @@ -407,7 +407,7 @@ def coords_to_vector(self, vector, coords_start=2 * RIGHT + 2 * UP, clean_up=Tru ), FadeOut(array.get_brackets()), ) - y_coord, brackets = self.get_mobjects_from_last_animation() + y_coord, brackets = self.mobjects_from_last_animation self.play(ShowCreation(y_line)) self.play(ShowCreation(arrow)) self.wait() From a7fb0eea2342095eb81ce725fffc63b9cbaa3a1b Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Tue, 6 Oct 2020 21:10:04 -0700 Subject: [PATCH 07/25] Remove Scene.force_skipping() --- manim/scene/scene.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 14f40bac06..667fc91297 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -971,22 +971,6 @@ def wait_until(self, stop_condition, max_time=60): """ self.wait(max_time, stop_condition=stop_condition) - def force_skipping(self): - """ - This forces the skipping of animations, - by setting original_skipping_status to - whatever skip_animations was, and setting - skip_animations to True. - - Returns - ------- - Scene - The Scene, with skipping turned on. - """ - self.original_skipping_status = file_writer_config["skip_animations"] - file_writer_config["skip_animations"] = True - return self - def revert_to_original_skipping_status(self): """ Forces the scene to go back to its original skipping status, From 304fd4dd79adfecc818fc5621973257ccc939248 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Wed, 7 Oct 2020 01:43:46 -0700 Subject: [PATCH 08/25] Move Scene.add_frame() --- manim/renderer/cairo_renderer.py | 18 ++++++++++ manim/scene/scene.py | 56 ++++++++++++-------------------- 2 files changed, 38 insertions(+), 36 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 96c0fbb0ea..e71e9bd113 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -62,6 +62,24 @@ def get_frame(self): """ return np.array(self.camera.pixel_array) + def add_frame(self, frame, num_frames=1): + """ + Adds a frame to the video_file_stream + + Parameters + ---------- + frame : numpy.ndarray + The frame to add, as a pixel array. + num_frames: int + The number of times to add frame. + """ + dt = 1 / self.camera.frame_rate + self.scene.increment_time(num_frames * dt) + if file_writer_config["skip_animations"]: + return + for _ in range(num_frames): + self.file_writer.write_frame(frame) + def update_skipping_status(self): """ This method is used internally to check if the current diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 667fc91297..1d7afb326d 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -757,7 +757,7 @@ def progress_through_animations(self): for t in self.get_animation_time_progression(self.animations): self.update_animation_to_time(t) self.renderer.update_frame(self.moving_mobjects, self.static_image) - self.add_frame(self.renderer.get_frame()) + self.renderer.add_frame(self.renderer.get_frame()) def update_animation_to_time(self, t): """ @@ -805,6 +805,21 @@ def play(self, *args, **kwargs): self.cached_play(*args, **kwargs) self.num_plays += 1 + def revert_to_original_skipping_status(self): + """ + Forces the scene to go back to its original skipping status, + by setting skip_animations to whatever it reads + from original_skipping_status. + + Returns + ------- + Scene + The Scene, with the original skipping status. + """ + if hasattr(self, "original_skipping_status"): + file_writer_config["skip_animations"] = self.original_skipping_status + return self + def cached_play(self, *args, **kwargs): self.revert_to_original_skipping_status() self.renderer.update_skipping_status() @@ -888,7 +903,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): for t in time_progression: self.update_animation_to_time(t) self.renderer.update_frame() - self.add_frame(self.renderer.get_frame()) + self.renderer.add_frame(self.renderer.get_frame()) if stop_condition is not None and stop_condition(): time_progression.close() break @@ -898,7 +913,9 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): else: self.renderer.update_frame() dt = 1 / self.camera.frame_rate - self.add_frame(self.renderer.get_frame(), num_frames=int(duration / dt)) + self.renderer.add_frame( + self.renderer.get_frame(), num_frames=int(duration / dt) + ) return self def clean_up_animations(self, *animations): @@ -971,39 +988,6 @@ def wait_until(self, stop_condition, max_time=60): """ self.wait(max_time, stop_condition=stop_condition) - def revert_to_original_skipping_status(self): - """ - Forces the scene to go back to its original skipping status, - by setting skip_animations to whatever it reads - from original_skipping_status. - - Returns - ------- - Scene - The Scene, with the original skipping status. - """ - if hasattr(self, "original_skipping_status"): - file_writer_config["skip_animations"] = self.original_skipping_status - return self - - def add_frame(self, frame, num_frames=1): - """ - Adds a frame to the video_file_stream - - Parameters - ---------- - frame : numpy.ndarray - The frame to add, as a pixel array. - num_frames: int - The number of times to add frame. - """ - dt = 1 / self.camera.frame_rate - self.increment_time(num_frames * dt) - if file_writer_config["skip_animations"]: - return - for _ in range(num_frames): - self.file_writer.write_frame(frame) - def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): """ This method is used to add a sound to the animation. From 7af79988654844d16e3de13ab98fa1966bff9847 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Wed, 7 Oct 2020 02:08:45 -0700 Subject: [PATCH 09/25] Move Scene.show_frame() --- manim/renderer/cairo_renderer.py | 8 ++++++++ manim/scene/scene.py | 8 -------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index e71e9bd113..b3696c40a2 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -95,3 +95,11 @@ def update_skipping_status(self): if self.scene.num_plays > file_writer_config["upto_animation_number"]: file_writer_config["skip_animations"] = True raise EndSceneEarlyException() + + def show_frame(self): + """ + Opens the current frame in the Default Image Viewer + of your system. + """ + self.update_frame(ignore_skipping=True) + self.camera.get_image().show() diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 1d7afb326d..0fff2f3c13 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -1008,11 +1008,3 @@ def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): return time = self.time + time_offset self.file_writer.add_sound(sound_file, time, gain, **kwargs) - - def show_frame(self): - """ - Opens the current frame in the Default Image Viewer - of your system. - """ - self.renderer.update_frame(ignore_skipping=True) - self.camera.get_image().show() From c74256ce2f4fe40c23021ca98e553182c9ea8ffd Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Thu, 8 Oct 2020 19:09:09 -0700 Subject: [PATCH 10/25] Move more stuff out of Scene --- manim/renderer/cairo_renderer.py | 134 ++++++++++++++++-- manim/scene/scene.py | 101 +------------ manim/scene/scene_file_writer.py | 6 +- .../logs_data/BasicSceneLoggingTest.txt | 2 +- 4 files changed, 135 insertions(+), 108 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index b3696c40a2..97a42931c7 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,14 +1,108 @@ import numpy as np -from .. import file_writer_config +from .. import camera_config, file_writer_config, logger from ..utils.iterables import list_update from ..utils.exceptions import EndSceneEarlyException +from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call +from ..constants import DEFAULT_WAIT_TIME + + +def handle_caching_play(func): + """ + Decorator that returns a wrapped version of func that will compute the hash of the play invocation. + + The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. + + Parameters + ---------- + func : Callable[[...], None] + The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`. + """ + + def wrapper(self, *args, **kwargs): + self.revert_to_original_skipping_status() + self.update_skipping_status() + animations = self.scene.compile_play_args_to_animation_list(*args, **kwargs) + self.scene.add_mobjects_from_animations(animations) + if file_writer_config["skip_animations"]: + logger.debug(f"Skipping animation {self.num_plays}") + func(self, *args, **kwargs) + return + if not file_writer_config["disable_caching"]: + mobjects_on_scene = self.scene.get_mobjects() + hash_play = get_hash_from_play_call( + self, self.camera, animations, mobjects_on_scene + ) + self.play_hashes_list.append(hash_play) + if self.file_writer.is_already_cached(hash_play): + logger.info( + f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", + {"hash_play": hash_play}, + ) + file_writer_config["skip_animations"] = True + else: + hash_play = "uncached_{:05}".format(self.num_plays) + self.play_hashes_list.append(hash_play) + func(self, *args, **kwargs) + + return wrapper + + +def handle_play_like_call(func): + """ + This method is used internally to wrap the + passed function, into a function that + actually writes to the video stream. + Simultaneously, it also adds to the number + of animations played. + + Parameters + ---------- + func : function + The play() like function that has to be + written to the video file stream. + + Returns + ------- + function + The play() like function that can now write + to the video file stream. + """ + + def wrapper(self, *args, **kwargs): + allow_write = not file_writer_config["skip_animations"] + self.file_writer.begin_animation(allow_write) + func(self, *args, **kwargs) + self.file_writer.end_animation(allow_write) + self.num_plays += 1 + + return wrapper class CairoRenderer: + """A renderer using Cairo. + + num_plays : Number of play() functions in the scene. + time: time elapsed since initialisation of scene. + """ + def __init__(self, scene, camera, file_writer): - self.scene = scene self.camera = camera self.file_writer = file_writer + self.scene = scene + self.original_skipping_status = file_writer_config["skip_animations"] + self.play_hashes_list = [] + self.num_plays = 0 + self.time = 0 + + @handle_caching_play + @handle_play_like_call + def play(self, *args, **kwargs): + self.scene.play_internal(*args, **kwargs) + + @handle_caching_play + @handle_play_like_call + def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): + self.scene.wait_internal(duration=duration, stop_condition=stop_condition) def update_frame( # TODO Description in Docstring self, @@ -74,12 +168,20 @@ def add_frame(self, frame, num_frames=1): The number of times to add frame. """ dt = 1 / self.camera.frame_rate - self.scene.increment_time(num_frames * dt) + self.time += num_frames * dt if file_writer_config["skip_animations"]: return for _ in range(num_frames): self.file_writer.write_frame(frame) + def show_frame(self): + """ + Opens the current frame in the Default Image Viewer + of your system. + """ + self.update_frame(ignore_skipping=True) + self.camera.get_image().show() + def update_skipping_status(self): """ This method is used internally to check if the current @@ -89,17 +191,29 @@ def update_skipping_status(self): raises an EndSceneEarlyException if they don't correspond. """ if file_writer_config["from_animation_number"]: - if self.scene.num_plays < file_writer_config["from_animation_number"]: + if self.num_plays < file_writer_config["from_animation_number"]: file_writer_config["skip_animations"] = True if file_writer_config["upto_animation_number"]: - if self.scene.num_plays > file_writer_config["upto_animation_number"]: + if self.num_plays > file_writer_config["upto_animation_number"]: file_writer_config["skip_animations"] = True raise EndSceneEarlyException() - def show_frame(self): + def revert_to_original_skipping_status(self): """ - Opens the current frame in the Default Image Viewer - of your system. + Forces the scene to go back to its original skipping status, + by setting skip_animations to whatever it reads + from original_skipping_status. + + Returns + ------- + Scene + The Scene, with the original skipping status. """ - self.update_frame(ignore_skipping=True) - self.camera.get_image().show() + if hasattr(self, "original_skipping_status"): + file_writer_config["skip_animations"] = self.original_skipping_status + return self + + def finish(self): + file_writer_config["skip_animations"] = False + self.file_writer.finish() + logger.info(f"Rendered {str(self.scene)}\nPlayed {self.num_plays} animations") diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 0fff2f3c13..458dd2cd2a 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -52,8 +52,6 @@ def construct(self): file_writer : The object that writes the animations in the scene to a video file. mobjects : The list of mobjects present in the scene. foreground_mobjects : List of mobjects explicitly in the foreground. - num_plays : Number of play() functions in the scene. - time: time elapsed since initialisation of scene. random_seed: The seed with which all random operations are done. """ @@ -73,15 +71,10 @@ def __init__(self, **kwargs): **file_writer_config, ) self.renderer = CairoRenderer(self, self.camera, self.file_writer) - self.play_hashes_list = [] self.mobjects = [] - self.original_skipping_status = file_writer_config["skip_animations"] # TODO, remove need for foreground mobjects self.foreground_mobjects = [] - self.num_plays = 0 - self.time = 0 - self.original_skipping_status = file_writer_config["skip_animations"] if self.random_seed is not None: random.seed(self.random_seed) np.random.seed(self.random_seed) @@ -96,13 +89,8 @@ def render(self): self.construct() except EndSceneEarlyException: pass - self.tear_down() - # We have to reset these settings in case of multiple renders. - file_writer_config["skip_animations"] = False - - self.file_writer.finish() - self.print_end_message() + self.renderer.finish() def setup(self): """ @@ -130,15 +118,6 @@ def construct(self): def __str__(self): return self.__class__.__name__ - def print_end_message(self): - """ - Used internally to print the number of - animations played after the scene ends, - as well as the name of the scene rendered - (useful when using the `-a` option). - """ - logger.info(f"Rendered {str(self)}\nPlayed {self.num_plays} animations") - def set_variables_as_attrs(self, *objects, **newly_named_objects): """ This method is slightly hacky, making it a little easier @@ -199,18 +178,6 @@ def should_update_mobjects(self): ### - def increment_time(self, d_time): - """ - Increments the time elapsed after intialisation of scene by - passed "d_time". - - Parameters - ---------- - d_time : int or float - Time in seconds to increment by. - """ - self.time += d_time - ### def get_top_level_mobjects(self): @@ -646,7 +613,7 @@ def get_animation_time_progression(self, animations): time_progression.set_description( "".join( [ - "Animation {}: ".format(self.num_plays), + "Animation {}: ".format(self.renderer.num_plays), str(animations[0]), (", etc." if len(animations) > 1 else ""), ] @@ -799,66 +766,10 @@ def finish_animations(self, animations): self.update_mobjects(0) def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.play(duration=duration, stop_condition=stop_condition) + self.renderer.wait(duration=duration, stop_condition=stop_condition) def play(self, *args, **kwargs): - self.cached_play(*args, **kwargs) - self.num_plays += 1 - - def revert_to_original_skipping_status(self): - """ - Forces the scene to go back to its original skipping status, - by setting skip_animations to whatever it reads - from original_skipping_status. - - Returns - ------- - Scene - The Scene, with the original skipping status. - """ - if hasattr(self, "original_skipping_status"): - file_writer_config["skip_animations"] = self.original_skipping_status - return self - - def cached_play(self, *args, **kwargs): - self.revert_to_original_skipping_status() - self.renderer.update_skipping_status() - animations = self.compile_play_args_to_animation_list(*args, **kwargs) - self.add_mobjects_from_animations(animations) - if file_writer_config["skip_animations"]: - logger.debug(f"Skipping animation {self.num_plays}") - self.file_writer_wrapped_play(*args, **kwargs) - return - if not file_writer_config["disable_caching"]: - mobjects_on_scene = self.get_mobjects() - hash_play = get_hash_from_play_call( - self, self.camera, animations, mobjects_on_scene - ) - self.play_hashes_list.append(hash_play) - if self.file_writer.is_already_cached(hash_play): - logger.info( - f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", - {"hash_play": hash_play}, - ) - file_writer_config["skip_animations"] = True - else: - hash_play = "uncached_{:05}".format(self.num_plays) - self.play_hashes_list.append(hash_play) - self.file_writer_wrapped_play(*args, **kwargs) - - def file_writer_wrapped_play(self, *args, **kwargs): - allow_write = not file_writer_config["skip_animations"] - self.file_writer.begin_animation(allow_write) - - self.play_or_wait(*args, **kwargs) - - self.file_writer.end_animation(allow_write) - - def play_or_wait(self, *args, **kwargs): - if "duration" in kwargs: - self.wait_internal(**kwargs) - else: - self.play_internal(*args, **kwargs) + self.renderer.play(*args, **kwargs) def play_internal(self, *args, **kwargs): """ @@ -969,7 +880,9 @@ def get_wait_time_progression(self, duration, stop_condition): ) else: time_progression = self.get_time_progression(duration) - time_progression.set_description("Waiting {}".format(self.num_plays)) + time_progression.set_description( + "Waiting {}".format(self.renderer.num_plays) + ) return time_progression def wait_until(self, stop_condition, max_time=60): diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index bbe86b609d..4c269a368c 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -202,7 +202,7 @@ def get_next_partial_movie_path(self): result = os.path.join( self.partial_movie_directory, "{}{}".format( - self.scene.play_hashes_list[self.index_partial_movie_file], + self.scene.renderer.play_hashes_list[self.index_partial_movie_file], file_writer_config["movie_file_extension"], ), ) @@ -461,7 +461,7 @@ def close_movie_pipe(self): self.partial_movie_file_path, ) logger.info( - f"Animation {self.scene.num_plays} : Partial movie file written in %(path)s", + f"Animation {self.scene.renderer.num_plays} : Partial movie file written in %(path)s", {"path": {self.partial_movie_file_path}}, ) @@ -502,7 +502,7 @@ def combine_movie_files(self): self.partial_movie_directory, "{}{}".format(hash_play, file_writer_config["movie_file_extension"]), ) - for hash_play in self.scene.play_hashes_list + for hash_play in self.scene.renderer.play_hashes_list ] if len(partial_movie_files) == 0: logger.error("No animations in this scene") diff --git a/tests/control_data/logs_data/BasicSceneLoggingTest.txt b/tests/control_data/logs_data/BasicSceneLoggingTest.txt index e7595ef9bc..c0ae14ac52 100644 --- a/tests/control_data/logs_data/BasicSceneLoggingTest.txt +++ b/tests/control_data/logs_data/BasicSceneLoggingTest.txt @@ -3,4 +3,4 @@ {"levelname": "DEBUG", "module": "hashing", "message": "Hashing done in <> s."} {"levelname": "INFO", "module": "scene_file_writer", "message": "Animation 0 : Partial movie file written in <>"} {"levelname": "INFO", "module": "scene_file_writer", "message": "\nFile ready at <>\n"} -{"levelname": "INFO", "module": "scene", "message": "Rendered SquareToCircle\nPlayed 1 animations"} +{"levelname": "INFO", "module": "cairo_renderer", "message": "Rendered SquareToCircle\nPlayed 1 animations"} From d9eee5382cd8ac16b0cc77f0e4ca16ba130797b3 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Fri, 9 Oct 2020 00:58:35 -0700 Subject: [PATCH 11/25] Move Scene.file_writer --- manim/__main__.py | 2 +- manim/renderer/cairo_renderer.py | 8 ++++++-- manim/scene/scene.py | 8 ++------ 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index 2450229bca..1d6fd193a7 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -79,7 +79,7 @@ def main(): else: scene = SceneClass() scene.render() - open_file_if_needed(scene.file_writer) + open_file_if_needed(scene.renderer.file_writer) except Exception: print("\n\n") traceback.print_exc() diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 97a42931c7..1d3c2d38cd 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -4,6 +4,7 @@ from ..utils.exceptions import EndSceneEarlyException from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call from ..constants import DEFAULT_WAIT_TIME +from ..scene.scene_file_writer import SceneFileWriter def handle_caching_play(func): @@ -85,9 +86,12 @@ class CairoRenderer: time: time elapsed since initialisation of scene. """ - def __init__(self, scene, camera, file_writer): + def __init__(self, scene, camera): self.camera = camera - self.file_writer = file_writer + self.file_writer = SceneFileWriter( + scene, + **file_writer_config, + ) self.scene = scene self.original_skipping_status = file_writer_config["skip_animations"] self.play_hashes_list = [] diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 458dd2cd2a..f302daf618 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -66,11 +66,7 @@ def construct(self): def __init__(self, **kwargs): Container.__init__(self, **kwargs) self.camera = self.camera_class(**camera_config) - self.file_writer = SceneFileWriter( - self, - **file_writer_config, - ) - self.renderer = CairoRenderer(self, self.camera, self.file_writer) + self.renderer = CairoRenderer(self, self.camera) self.mobjects = [] # TODO, remove need for foreground mobjects @@ -920,4 +916,4 @@ def add_sound(self, sound_file, time_offset=0, gain=None, **kwargs): if file_writer_config["skip_animations"]: return time = self.time + time_offset - self.file_writer.add_sound(sound_file, time, gain, **kwargs) + self.renderer.file_writer.add_sound(sound_file, time, gain, **kwargs) From eddbb06e25a183f00cdfd564f209d6a18f28ee91 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Fri, 9 Oct 2020 02:53:38 -0700 Subject: [PATCH 12/25] Remove some Scene references from SceneFileWriter --- manim/renderer/cairo_renderer.py | 22 +++++++++++++++++++++- manim/scene/scene_file_writer.py | 18 ++++++++---------- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 1d3c2d38cd..a7b8c278f4 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,5 +1,5 @@ import numpy as np -from .. import camera_config, file_writer_config, logger +from .. import config, camera_config, file_writer_config, logger from ..utils.iterables import list_update from ..utils.exceptions import EndSceneEarlyException from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call @@ -88,7 +88,25 @@ class CairoRenderer: def __init__(self, scene, camera): self.camera = camera + + # All of the following are set to EITHER the value passed via kwargs, + # OR the value stored in the global config dict at the time of + # _instance construction_. Before, they were in the CONFIG dict, which + # is a class attribute and is defined at the time of _class + # definition_. This did not allow for creating two Cameras with + # different configurations in the same session. + self.video_quality_config = {} + for attr in [ + "pixel_height", + "pixel_width", + "frame_height", + "frame_width", + "frame_rate", + ]: + self.video_quality_config[attr] = camera_config.get(attr, config[attr]) + self.file_writer = SceneFileWriter( + self.video_quality_config, scene, **file_writer_config, ) @@ -220,4 +238,6 @@ def revert_to_original_skipping_status(self): def finish(self): file_writer_config["skip_animations"] = False self.file_writer.finish() + if file_writer_config["save_last_frame"]: + self.file_writer.save_final_image(self.camera.get_image()) logger.info(f"Rendered {str(self.scene)}\nPlayed {self.num_plays} animations") diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 4c269a368c..20d48e8337 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -39,8 +39,9 @@ class SceneFileWriter(object): The file-type extension of the outputted video. """ - def __init__(self, scene, **kwargs): + def __init__(self, video_quality_config, scene, **kwargs): digest_config(self, kwargs) + self.video_quality_config = video_quality_config self.scene = scene self.stream_lock = False self.init_output_directories() @@ -167,8 +168,8 @@ def get_resolution_directory(self): :class:`str` The name of the directory. """ - pixel_height = self.scene.camera.pixel_height - frame_rate = self.scene.camera.frame_rate + pixel_height = self.video_quality_config["pixel_height"] + frame_rate = self.video_quality_config["frame_rate"] return "{}p{}".format(pixel_height, frame_rate) # Directory getters @@ -369,7 +370,7 @@ def idle_stream(self): self.add_frame(*[frame] * n_frames) b = datetime.datetime.now() time_diff = (b - a).total_seconds() - frame_duration = 1 / self.scene.camera.frame_rate + frame_duration = 1 / self.video_quality_config["frame_rate"] if time_diff < frame_duration: sleep(frame_duration - time_diff) @@ -389,9 +390,6 @@ def finish(self): self.flush_cache_directory() else: self.clean_cache() - if file_writer_config["save_last_frame"]: - self.scene.update_frame(ignore_skipping=True) - self.save_final_image(self.scene.camera.get_image()) def open_movie_pipe(self): """ @@ -408,9 +406,9 @@ def open_movie_pipe(self): self.partial_movie_file_path = file_path self.temp_partial_movie_file_path = temp_file_path - fps = self.scene.camera.frame_rate - height = self.scene.camera.pixel_height - width = self.scene.camera.pixel_width + fps = self.video_quality_config["frame_rate"] + height = self.video_quality_config["pixel_height"] + width = self.video_quality_config["pixel_width"] command = [ FFMPEG_BIN, From f8a55aa8cc8b326987d80f210218ff843125e942 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Fri, 9 Oct 2020 03:07:34 -0700 Subject: [PATCH 13/25] Update ThreeDScene, move Scene.camera --- manim/renderer/cairo_renderer.py | 4 +-- manim/scene/scene.py | 7 ++-- manim/scene/three_d_scene.py | 56 +++++++++++++++++--------------- 3 files changed, 34 insertions(+), 33 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index a7b8c278f4..6c811c7b0f 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -86,8 +86,8 @@ class CairoRenderer: time: time elapsed since initialisation of scene. """ - def __init__(self, scene, camera): - self.camera = camera + def __init__(self, scene, camera_class): + self.camera = camera_class(**camera_config) # All of the following are set to EITHER the value passed via kwargs, # OR the value stored in the global config dict at the time of diff --git a/manim/scene/scene.py b/manim/scene/scene.py index f302daf618..5630666492 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -65,8 +65,7 @@ def construct(self): def __init__(self, **kwargs): Container.__init__(self, **kwargs) - self.camera = self.camera_class(**camera_config) - self.renderer = CairoRenderer(self, self.camera) + self.renderer = CairoRenderer(self, self.camera_class) self.mobjects = [] # TODO, remove need for foreground mobjects @@ -556,7 +555,7 @@ def get_time_progression( if file_writer_config["skip_animations"] and not override_skip_animations: times = [run_time] else: - step = 1 / self.camera.frame_rate + step = 1 / self.renderer.camera.frame_rate times = np.arange(0, run_time, step) time_progression = ProgressDisplay( times, @@ -819,7 +818,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): return self else: self.renderer.update_frame() - dt = 1 / self.camera.frame_rate + dt = 1 / self.renderer.camera.frame_rate self.renderer.add_frame( self.renderer.get_frame(), num_frames=int(duration / dt) ) diff --git a/manim/scene/three_d_scene.py b/manim/scene/three_d_scene.py index 26dbf5ed20..5065899f5d 100644 --- a/manim/scene/three_d_scene.py +++ b/manim/scene/three_d_scene.py @@ -53,13 +53,13 @@ def set_camera_orientation(self, phi=None, theta=None, distance=None, gamma=None The rotation of the camera about the vector from the ORIGIN to the Camera. """ if phi is not None: - self.camera.set_phi(phi) + self.renderer.camera.set_phi(phi) if theta is not None: - self.camera.set_theta(theta) + self.renderer.camera.set_theta(theta) if distance is not None: - self.camera.set_distance(distance) + self.renderer.camera.set_distance(distance) if gamma is not None: - self.camera.set_gamma(gamma) + self.renderer.camera.set_gamma(gamma) def begin_ambient_camera_rotation(self, rate=0.02): """ @@ -74,17 +74,17 @@ def begin_ambient_camera_rotation(self, rate=0.02): """ # TODO, use a ValueTracker for rate, so that it # can begin and end smoothly - self.camera.theta_tracker.add_updater( + self.renderer.camera.theta_tracker.add_updater( lambda m, dt: m.increment_value(rate * dt) ) - self.add(self.camera.theta_tracker) + self.add(self.renderer.camera.theta_tracker) def stop_ambient_camera_rotation(self): """ This method stops all ambient camera rotation. """ - self.camera.theta_tracker.clear_updaters() - self.remove(self.camera.theta_tracker) + self.renderer.camera.theta_tracker.clear_updaters() + self.remove(self.renderer.camera.theta_tracker) def begin_3dillusion_camera_rotation( self, rate=1, origin_theta=-60 * DEGREES, origin_phi=75 * DEGREES @@ -96,8 +96,8 @@ def uptate_theta(m, dt): val_for_left_right = 0.2 * np.sin(val_tracker_theta.get_value()) return m.set_value(origin_theta + val_for_left_right) - self.camera.theta_tracker.add_updater(uptate_theta) - self.add(self.camera.theta_tracker) + self.renderer.camera.theta_tracker.add_updater(uptate_theta) + self.add(self.renderer.camera.theta_tracker) val_tracker_phi = ValueTracker(0) @@ -106,17 +106,17 @@ def update_phi(m, dt): val_for_up_down = 0.1 * np.cos(val_tracker_phi.get_value()) return m.set_value(origin_phi + val_for_up_down) - self.camera.phi_tracker.add_updater(update_phi) - self.add(self.camera.phi_tracker) + self.renderer.camera.phi_tracker.add_updater(update_phi) + self.add(self.renderer.camera.phi_tracker) def stop_3dillusion_camera_rotation(self): """ This method stops all illusion camera rotations. """ - self.camera.theta_tracker.clear_updaters() - self.remove(self.camera.theta_tracker) - self.camera.phi_tracker.clear_updaters() - self.remove(self.camera.phi_tracker) + self.renderer.camera.theta_tracker.clear_updaters() + self.remove(self.renderer.camera.theta_tracker) + self.renderer.camera.phi_tracker.clear_updaters() + self.remove(self.renderer.camera.phi_tracker) def move_camera( self, @@ -155,16 +155,18 @@ def move_camera( """ anims = [] value_tracker_pairs = [ - (phi, self.camera.phi_tracker), - (theta, self.camera.theta_tracker), - (distance, self.camera.distance_tracker), - (gamma, self.camera.gamma_tracker), + (phi, self.renderer.camera.phi_tracker), + (theta, self.renderer.camera.theta_tracker), + (distance, self.renderer.camera.distance_tracker), + (gamma, self.renderer.camera.gamma_tracker), ] for value, tracker in value_tracker_pairs: if value is not None: anims.append(ApplyMethod(tracker.set_value, value, **kwargs)) if frame_center is not None: - anims.append(ApplyMethod(self.camera.frame_center.move_to, frame_center)) + anims.append( + ApplyMethod(self.renderer.camera.frame_center.move_to, frame_center) + ) self.play(*anims + added_anims) @@ -179,7 +181,7 @@ def get_moving_mobjects(self, *animations): The animations whose mobjects will be checked. """ moving_mobjects = Scene.get_moving_mobjects(self, *animations) - camera_mobjects = self.camera.get_value_trackers() + camera_mobjects = self.renderer.camera.get_value_trackers() if any([cm in moving_mobjects for cm in camera_mobjects]): return self.mobjects return moving_mobjects @@ -203,7 +205,7 @@ def add_fixed_orientation_mobjects(self, *mobjects, **kwargs): center_func : function """ self.add(*mobjects) - self.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs) + self.renderer.camera.add_fixed_orientation_mobjects(*mobjects, **kwargs) def add_fixed_in_frame_mobjects(self, *mobjects): """ @@ -218,7 +220,7 @@ def add_fixed_in_frame_mobjects(self, *mobjects): The Mobjects whose orientation must be fixed. """ self.add(*mobjects) - self.camera.add_fixed_in_frame_mobjects(*mobjects) + self.renderer.camera.add_fixed_in_frame_mobjects(*mobjects) def remove_fixed_orientation_mobjects(self, *mobjects): """ @@ -232,7 +234,7 @@ def remove_fixed_orientation_mobjects(self, *mobjects): *mobjects : Mobjects The Mobjects whose orientation must be unfixed. """ - self.camera.remove_fixed_orientation_mobjects(*mobjects) + self.renderer.camera.remove_fixed_orientation_mobjects(*mobjects) def remove_fixed_in_frame_mobjects(self, *mobjects): """ @@ -245,7 +247,7 @@ def remove_fixed_in_frame_mobjects(self, *mobjects): *mobjects : Mobjects The Mobjects whose position and orientation must be unfixed. """ - self.camera.remove_fixed_in_frame_mobjects(*mobjects) + self.renderer.camera.remove_fixed_in_frame_mobjects(*mobjects) ## def set_to_default_angled_camera_orientation(self, **kwargs): @@ -305,7 +307,7 @@ class SpecialThreeDScene(ThreeDScene): def __init__(self, **kwargs): digest_config(self, kwargs) - if self.camera_config["pixel_width"] == config["pixel_width"]: + if self.renderer.camera_config["pixel_width"] == config["pixel_width"]: config = {} else: config = self.low_quality_config From 97f07158e3d3f8fd7ecd77d14cccdea74607a914 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Fri, 9 Oct 2020 03:35:20 -0700 Subject: [PATCH 14/25] Update Camera test --- manim/__init__.py | 2 ++ manim/camera/camera.py | 2 +- manim/renderer/cairo_renderer.py | 8 +++----- tests/test_camera.py | 16 ---------------- tests/test_renderer.py | 27 +++++++++++++++++++++++++++ 5 files changed, 33 insertions(+), 22 deletions(-) delete mode 100644 tests/test_camera.py create mode 100644 tests/test_renderer.py diff --git a/manim/__init__.py b/manim/__init__.py index 0f6b70bfd2..9d3c648927 100644 --- a/manim/__init__.py +++ b/manim/__init__.py @@ -21,6 +21,8 @@ from .animation.transform import * from .animation.update import * +from .renderer.cairo_renderer import * + from .camera.camera import * from .camera.mapping_camera import * from .camera.moving_camera import * diff --git a/manim/camera/camera.py b/manim/camera/camera.py index e2aa8948e7..6fb59497e9 100644 --- a/manim/camera/camera.py +++ b/manim/camera/camera.py @@ -67,7 +67,7 @@ class Camera(object): "use_z_index": True, } - def __init__(self, background=None, **kwargs): + def __init__(self, video_quality_config, background=None, **kwargs): """Initialises the Camera. Parameters diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 6c811c7b0f..9ed5c87f8e 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -86,9 +86,7 @@ class CairoRenderer: time: time elapsed since initialisation of scene. """ - def __init__(self, scene, camera_class): - self.camera = camera_class(**camera_config) - + def __init__(self, scene, camera_class, **kwargs): # All of the following are set to EITHER the value passed via kwargs, # OR the value stored in the global config dict at the time of # _instance construction_. Before, they were in the CONFIG dict, which @@ -103,8 +101,8 @@ def __init__(self, scene, camera_class): "frame_width", "frame_rate", ]: - self.video_quality_config[attr] = camera_config.get(attr, config[attr]) - + self.video_quality_config[attr] = kwargs.get(attr, config[attr]) + self.camera = camera_class(self.video_quality_config, **camera_config) self.file_writer = SceneFileWriter( self.video_quality_config, scene, diff --git a/tests/test_camera.py b/tests/test_camera.py deleted file mode 100644 index dd8f00f5bb..0000000000 --- a/tests/test_camera.py +++ /dev/null @@ -1,16 +0,0 @@ -import pytest -from manim import Camera, tempconfig, config - - -def test_camera(): - """Test that Camera instances initialize to the correct config.""" - # by default, use the config - assert Camera().frame_width == config["frame_width"] - # init args override config - assert Camera(frame_width=10).frame_width == 10 - - # if config changes, reflect those changes - with tempconfig({"frame_width": 100}): - assert Camera().frame_width == 100 - # ..init args still override new config - assert Camera(frame_width=10).frame_width == 10 diff --git a/tests/test_renderer.py b/tests/test_renderer.py new file mode 100644 index 0000000000..6995b0067e --- /dev/null +++ b/tests/test_renderer.py @@ -0,0 +1,27 @@ +import pytest +from manim import CairoRenderer, Camera, tempconfig, config + + +def test_renderer(): + """Test that CairoRenderer instances initialize to the correct config.""" + # by default, use the config + assert ( + CairoRenderer(None, Camera).video_quality_config["frame_width"] + == config["frame_width"] + ) + # init args override config + assert ( + CairoRenderer(None, Camera, frame_width=10).video_quality_config["frame_width"] + == 10 + ) + + # if config changes, reflect those changes + with tempconfig({"frame_width": 100}): + assert CairoRenderer(None, Camera).video_quality_config["frame_width"] == 100 + # ..init args still override new config + assert ( + CairoRenderer(None, Camera, frame_width=10).video_quality_config[ + "frame_width" + ] + == 10 + ) From cab33963abebaae8080e343a111ca21615d7d679 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sat, 10 Oct 2020 13:23:41 -0700 Subject: [PATCH 15/25] Use wait-specific caching function --- manim/renderer/cairo_renderer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index b2af873f83..ac91f5882b 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -81,7 +81,7 @@ def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): return if not file_writer_config["disable_caching"]: hash_wait = get_hash_from_wait_call( - self, self.camera, duration, stop_condition, self.get_mobjects() + self, self.camera, duration, stop_condition, self.scene.get_mobjects() ) if self.file_writer.is_already_cached(hash_wait): logger.info( @@ -172,7 +172,7 @@ def __init__(self, scene, camera_class, **kwargs): def play(self, *args, **kwargs): self.scene.play_internal(*args, **kwargs) - @handle_caching_play + @handle_caching_wait @handle_play_like_call def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): self.scene.wait_internal(duration=duration, stop_condition=stop_condition) From 067f0c123eacfdf30647b5ada65f3886a5b8c7ca Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sat, 10 Oct 2020 13:39:20 -0700 Subject: [PATCH 16/25] Add renderer/__init__.py --- manim/renderer/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 manim/renderer/__init__.py diff --git a/manim/renderer/__init__.py b/manim/renderer/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From a74313f78629bc3c597e1b9077c86b3168fcfb09 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sat, 10 Oct 2020 14:57:29 -0700 Subject: [PATCH 17/25] Update Camera variants --- docs/source/examples/3d.rst | 2 +- manim/camera/moving_camera.py | 4 ++-- manim/camera/multi_camera.py | 11 +++++++---- manim/scene/moving_camera_scene.py | 11 +++++------ manim/scene/zoomed_scene.py | 4 ++-- 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/docs/source/examples/3d.rst b/docs/source/examples/3d.rst index 6a46f018df..5ffddca167 100644 --- a/docs/source/examples/3d.rst +++ b/docs/source/examples/3d.rst @@ -32,7 +32,7 @@ ]), v_min=0, v_max=TAU, u_min=-PI / 2, u_max=PI / 2, checkerboard_colors=[RED_D, RED_E], resolution=(15, 32) ) - self.camera.light_source.move_to(3*IN) # changes the source of the light + self.renderer.camera.light_source.move_to(3*IN) # changes the source of the light self.set_camera_orientation(phi=75 * DEGREES, theta=30 * DEGREES) self.add(axes, sphere) diff --git a/manim/camera/moving_camera.py b/manim/camera/moving_camera.py index f5d649f9e6..9cf891ff38 100644 --- a/manim/camera/moving_camera.py +++ b/manim/camera/moving_camera.py @@ -46,7 +46,7 @@ class MovingCamera(Camera): "default_frame_stroke_width": 0, } - def __init__(self, frame=None, **kwargs): + def __init__(self, video_quality_config, frame=None, **kwargs): """ frame is a Mobject, (should almost certainly be a rectangle) determining which region of space the camera displys @@ -59,7 +59,7 @@ def __init__(self, frame=None, **kwargs): self.default_frame_stroke_width, ) self.frame = frame - Camera.__init__(self, **kwargs) + Camera.__init__(self, video_quality_config, **kwargs) # TODO, make these work for a rotated frame @property diff --git a/manim/camera/multi_camera.py b/manim/camera/multi_camera.py index a503a849a8..1cec584620 100644 --- a/manim/camera/multi_camera.py +++ b/manim/camera/multi_camera.py @@ -14,7 +14,9 @@ class MultiCamera(MovingCamera): "allow_cameras_to_capture_their_own_display": False, } - def __init__(self, *image_mobjects_from_cameras, **kwargs): + def __init__( + self, video_quality_config, image_mobjects_from_cameras=None, **kwargs + ): """Initalises the MultiCamera Parameters: @@ -25,9 +27,10 @@ def __init__(self, *image_mobjects_from_cameras, **kwargs): Any valid keyword arguments of MovingCamera. """ self.image_mobjects_from_cameras = [] - for imfc in image_mobjects_from_cameras: - self.add_image_mobject_from_camera(imfc) - MovingCamera.__init__(self, **kwargs) + if image_mobjects_from_cameras is not None: + for imfc in image_mobjects_from_cameras: + self.add_image_mobject_from_camera(imfc) + MovingCamera.__init__(self, video_quality_config, **kwargs) def add_image_mobject_from_camera(self, image_mobject_from_camera): """Adds an ImageMobject that's been obtained from the camera diff --git a/manim/scene/moving_camera_scene.py b/manim/scene/moving_camera_scene.py index 31dc76a091..231df40c82 100644 --- a/manim/scene/moving_camera_scene.py +++ b/manim/scene/moving_camera_scene.py @@ -11,6 +11,7 @@ from ..camera.moving_camera import MovingCamera from ..scene.scene import Scene from ..utils.iterables import list_update +from ..utils.family import extract_mobject_family_members class MovingCameraScene(Scene): @@ -31,8 +32,8 @@ def setup(self): to set up the scene for proper use. """ Scene.setup(self) - assert isinstance(self.camera, MovingCamera) - self.camera_frame = self.camera.frame + assert isinstance(self.renderer.camera, MovingCamera) + self.camera_frame = self.renderer.camera.frame # Hmm, this currently relies on the fact that MovingCamera # willd default to a full-sized frame. Is that okay? return self @@ -48,10 +49,8 @@ def get_moving_mobjects(self, *animations): The Animations whose mobjects will be checked. """ moving_mobjects = Scene.get_moving_mobjects(self, *animations) - all_moving_mobjects = self.camera.extract_mobject_family_members( - moving_mobjects - ) - movement_indicators = self.camera.get_mobjects_indicating_movement() + all_moving_mobjects = extract_mobject_family_members(moving_mobjects) + movement_indicators = self.renderer.camera.get_mobjects_indicating_movement() for movement_indicator in movement_indicators: if movement_indicator in all_moving_mobjects: # When one of these is moving, the camera should diff --git a/manim/scene/zoomed_scene.py b/manim/scene/zoomed_scene.py index acbf1997fd..e88bfa5042 100644 --- a/manim/scene/zoomed_scene.py +++ b/manim/scene/zoomed_scene.py @@ -47,7 +47,7 @@ def setup(self): """ MovingCameraScene.setup(self) # Initialize camera and display - zoomed_camera = MovingCamera(**self.zoomed_camera_config) + zoomed_camera = MovingCamera({}, **self.zoomed_camera_config) zoomed_display = ImageMobjectFromCamera( zoomed_camera, **self.zoomed_camera_image_mobject_config ) @@ -81,7 +81,7 @@ def activate_zooming(self, animate=False): of the zoomed camera. """ self.zoom_activated = True - self.camera.add_image_mobject_from_camera(self.zoomed_display) + self.renderer.camera.add_image_mobject_from_camera(self.zoomed_display) if animate: self.play(self.get_zoom_in_animation()) self.play(self.get_zoomed_display_pop_out_animation()) From 57a2ead55e419411e5b00795d75306dc484e1f41 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 11 Oct 2020 13:04:07 -0700 Subject: [PATCH 18/25] Move caching functions --- manim/renderer/cairo_renderer.py | 95 +----------------- manim/utils/caching.py | 97 +++++++++++++++++++ .../logs_data/BasicSceneLoggingTest.txt | 2 +- 3 files changed, 99 insertions(+), 95 deletions(-) create mode 100644 manim/utils/caching.py diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index ac91f5882b..a72260372c 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -5,100 +5,7 @@ from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call from ..constants import DEFAULT_WAIT_TIME from ..scene.scene_file_writer import SceneFileWriter - - -def handle_caching_play(func): - """ - Decorator that returns a wrapped version of func that will compute the hash of the play invocation. - - The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. - - Parameters - ---------- - func : Callable[[...], None] - The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`. - """ - - def wrapper(self, *args, **kwargs): - self.revert_to_original_skipping_status() - self.update_skipping_status() - animations = self.scene.compile_play_args_to_animation_list(*args, **kwargs) - self.scene.add_mobjects_from_animations(animations) - if file_writer_config["skip_animations"]: - logger.debug(f"Skipping animation {self.num_plays}") - func(self, *args, **kwargs) - # If the animation is skipped, we mark its hash as None. - # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. - self.animations_hashes.append(None) - self.file_writer.add_partial_movie_file(None) - return - if not file_writer_config["disable_caching"]: - mobjects_on_scene = self.scene.get_mobjects() - hash_play = get_hash_from_play_call( - self, self.camera, animations, mobjects_on_scene - ) - if self.file_writer.is_already_cached(hash_play): - logger.info( - f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", - {"hash_play": hash_play}, - ) - file_writer_config["skip_animations"] = True - else: - hash_play = "uncached_{:05}".format(self.num_plays) - self.animations_hashes.append(hash_play) - self.file_writer.add_partial_movie_file(hash_play) - logger.debug( - "List of the first few animation hashes of the scene: %(h)s", - {"h": str(self.animations_hashes[:5])}, - ) - func(self, *args, **kwargs) - - return wrapper - - -def handle_caching_wait(func): - """ - Decorator that returns a wrapped version of func that will compute the hash of the wait invocation. - - The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. - - Parameters - ---------- - func : Callable[[...], None] - The wait like function that has to be written to the video file stream. Take the same parameters as `scene.wait`. - """ - - def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.revert_to_original_skipping_status() - self.update_skipping_status() - if file_writer_config["skip_animations"]: - logger.debug(f"Skipping wait {self.num_plays}") - func(self, duration, stop_condition) - # If the animation is skipped, we mark its hash as None. - # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. - self.animations_hashes.append(None) - self.file_writer.add_partial_movie_file(None) - return - if not file_writer_config["disable_caching"]: - hash_wait = get_hash_from_wait_call( - self, self.camera, duration, stop_condition, self.scene.get_mobjects() - ) - if self.file_writer.is_already_cached(hash_wait): - logger.info( - f"Wait {self.num_plays} : Using cached data (hash : {hash_wait})" - ) - file_writer_config["skip_animations"] = True - else: - hash_wait = "uncached_{:05}".format(self.num_plays) - self.animations_hashes.append(hash_wait) - self.file_writer.add_partial_movie_file(hash_wait) - logger.debug( - "Animations hashes list of the scene : (concatened to 5) %(h)s", - {"h": str(self.animations_hashes[:5])}, - ) - func(self, duration, stop_condition) - - return wrapper +from ..utils.caching import handle_caching_play, handle_caching_wait def handle_play_like_call(func): diff --git a/manim/utils/caching.py b/manim/utils/caching.py new file mode 100644 index 0000000000..e6f745aba0 --- /dev/null +++ b/manim/utils/caching.py @@ -0,0 +1,97 @@ +from .. import file_writer_config, logger +from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call +from ..constants import DEFAULT_WAIT_TIME + + +def handle_caching_play(func): + """ + Decorator that returns a wrapped version of func that will compute the hash of the play invocation. + + The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. + + Parameters + ---------- + func : Callable[[...], None] + The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`. + """ + + def wrapper(self, *args, **kwargs): + self.revert_to_original_skipping_status() + self.update_skipping_status() + animations = self.scene.compile_play_args_to_animation_list(*args, **kwargs) + self.scene.add_mobjects_from_animations(animations) + if file_writer_config["skip_animations"]: + logger.debug(f"Skipping animation {self.num_plays}") + func(self, *args, **kwargs) + # If the animation is skipped, we mark its hash as None. + # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. + self.animations_hashes.append(None) + self.file_writer.add_partial_movie_file(None) + return + if not file_writer_config["disable_caching"]: + mobjects_on_scene = self.scene.get_mobjects() + hash_play = get_hash_from_play_call( + self, self.camera, animations, mobjects_on_scene + ) + if self.file_writer.is_already_cached(hash_play): + logger.info( + f"Animation {self.num_plays} : Using cached data (hash : %(hash_play)s)", + {"hash_play": hash_play}, + ) + file_writer_config["skip_animations"] = True + else: + hash_play = "uncached_{:05}".format(self.num_plays) + self.animations_hashes.append(hash_play) + self.file_writer.add_partial_movie_file(hash_play) + logger.debug( + "List of the first few animation hashes of the scene: %(h)s", + {"h": str(self.animations_hashes[:5])}, + ) + func(self, *args, **kwargs) + + return wrapper + + +def handle_caching_wait(func): + """ + Decorator that returns a wrapped version of func that will compute the hash of the wait invocation. + + The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. + + Parameters + ---------- + func : Callable[[...], None] + The wait like function that has to be written to the video file stream. Take the same parameters as `scene.wait`. + """ + + def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): + self.revert_to_original_skipping_status() + self.update_skipping_status() + if file_writer_config["skip_animations"]: + logger.debug(f"Skipping wait {self.num_plays}") + func(self, duration, stop_condition) + # If the animation is skipped, we mark its hash as None. + # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. + self.animations_hashes.append(None) + self.file_writer.add_partial_movie_file(None) + return + if not file_writer_config["disable_caching"]: + hash_wait = get_hash_from_wait_call( + self, self.camera, duration, stop_condition, self.scene.get_mobjects() + ) + if self.file_writer.is_already_cached(hash_wait): + logger.info( + f"Wait {self.num_plays} : Using cached data (hash : {hash_wait})" + ) + file_writer_config["skip_animations"] = True + else: + hash_wait = "uncached_{:05}".format(self.num_plays) + self.animations_hashes.append(hash_wait) + self.file_writer.add_partial_movie_file(hash_wait) + logger.debug( + "Animations hashes list of the scene : (concatened to 5) %(h)s", + {"h": str(self.animations_hashes[:5])}, + ) + func(self, duration, stop_condition) + + return wrapper diff --git a/tests/control_data/logs_data/BasicSceneLoggingTest.txt b/tests/control_data/logs_data/BasicSceneLoggingTest.txt index 1fb4753b5c..576252e492 100644 --- a/tests/control_data/logs_data/BasicSceneLoggingTest.txt +++ b/tests/control_data/logs_data/BasicSceneLoggingTest.txt @@ -2,7 +2,7 @@ {"levelname": "DEBUG", "module": "hashing", "message": "Hashing ..."} {"levelname": "DEBUG", "module": "hashing", "message": "Hashing done in <> s."} {"levelname": "DEBUG", "module": "hashing", "message": "Hash generated : <>"} -{"levelname": "DEBUG", "module": "cairo_renderer", "message": "List of the first few animation hashes of the scene: <>"} +{"levelname": "DEBUG", "module": "caching", "message": "List of the first few animation hashes of the scene: <>"} {"levelname": "INFO", "module": "scene_file_writer", "message": "Animation 0 : Partial movie file written in <>"} {"levelname": "DEBUG", "module": "scene_file_writer", "message": "Partial movie files to combine (1 files): <>"} {"levelname": "INFO", "module": "scene_file_writer", "message": "\nFile ready at <>\n"} From 4051ee130e358877905fb791aef89cf5a995bc4d Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 11 Oct 2020 13:17:22 -0700 Subject: [PATCH 19/25] Pass Scene to play() and wait() --- manim/renderer/cairo_renderer.py | 11 +++++++++++ manim/scene/scene.py | 4 ++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index a72260372c..7cd477a374 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -8,6 +8,15 @@ from ..utils.caching import handle_caching_play, handle_caching_wait +def manage_scene_reference(func): + def wrapper(self, scene, *args, **kwargs): + setattr(self, "temp_scene_ref", scene) + func(self, *args, **kwargs) + delattr(self, "temp_scene_ref") + + return wrapper + + def handle_play_like_call(func): """ This method is used internally to wrap the @@ -74,11 +83,13 @@ def __init__(self, scene, camera_class, **kwargs): self.num_plays = 0 self.time = 0 + @manage_scene_reference @handle_caching_play @handle_play_like_call def play(self, *args, **kwargs): self.scene.play_internal(*args, **kwargs) + @manage_scene_reference @handle_caching_wait @handle_play_like_call def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 5630666492..e75f748ad7 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -761,10 +761,10 @@ def finish_animations(self, animations): self.update_mobjects(0) def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.renderer.wait(duration=duration, stop_condition=stop_condition) + self.renderer.wait(self, duration=duration, stop_condition=stop_condition) def play(self, *args, **kwargs): - self.renderer.play(*args, **kwargs) + self.renderer.play(self, *args, **kwargs) def play_internal(self, *args, **kwargs): """ From ca618a6d1836999f1d58d4596a572ee7df1950d7 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sat, 10 Oct 2020 21:54:59 -0700 Subject: [PATCH 20/25] Remove scene references from CairoRenderer --- manim/renderer/cairo_renderer.py | 11 +++++------ manim/scene/scene.py | 11 +++++++---- .../control_data/logs_data/BasicSceneLoggingTest.txt | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 7cd477a374..6da07022bd 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -10,9 +10,9 @@ def manage_scene_reference(func): def wrapper(self, scene, *args, **kwargs): - setattr(self, "temp_scene_ref", scene) + setattr(self, "scene", scene) func(self, *args, **kwargs) - delattr(self, "temp_scene_ref") + delattr(self, "scene") return wrapper @@ -77,7 +77,6 @@ def __init__(self, scene, camera_class, **kwargs): scene, **file_writer_config, ) - self.scene = scene self.original_skipping_status = file_writer_config["skip_animations"] self.animations_hashes = [] self.num_plays = 0 @@ -97,6 +96,7 @@ def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): def update_frame( # TODO Description in Docstring self, + scene, mobjects=None, background=None, include_submobjects=True, @@ -124,8 +124,8 @@ def update_frame( # TODO Description in Docstring return if mobjects is None: mobjects = list_update( - self.scene.mobjects, - self.scene.foreground_mobjects, + scene.mobjects, + scene.foreground_mobjects, ) if background is not None: self.camera.set_frame_to_background(background) @@ -209,4 +209,3 @@ def finish(self): self.file_writer.finish() if file_writer_config["save_last_frame"]: self.file_writer.save_final_image(self.camera.get_image()) - logger.info(f"Rendered {str(self.scene)}\nPlayed {self.num_plays} animations") diff --git a/manim/scene/scene.py b/manim/scene/scene.py index e75f748ad7..c244c6ebfa 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -86,6 +86,9 @@ def render(self): pass self.tear_down() self.renderer.finish() + logger.info( + f"Rendered {str(self)}\nPlayed {self.renderer.num_plays} animations" + ) def setup(self): """ @@ -718,7 +721,7 @@ def progress_through_animations(self): """ for t in self.get_animation_time_progression(self.animations): self.update_animation_to_time(t) - self.renderer.update_frame(self.moving_mobjects, self.static_image) + self.renderer.update_frame(self, self.moving_mobjects, self.static_image) self.renderer.add_frame(self.renderer.get_frame()) def update_animation_to_time(self, t): @@ -786,7 +789,7 @@ def play_internal(self, *args, **kwargs): # Paint all non-moving objects onto the screen, so they don't # have to be rendered every frame self.moving_mobjects = self.get_moving_mobjects(*self.animations) - self.renderer.update_frame(excluded_mobjects=self.moving_mobjects) + self.renderer.update_frame(self, excluded_mobjects=self.moving_mobjects) self.static_image = self.renderer.get_frame() self.last_t = 0 self.run_time = self.get_run_time(self.animations) @@ -808,7 +811,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): # the same way Scene.play does for t in time_progression: self.update_animation_to_time(t) - self.renderer.update_frame() + self.renderer.update_frame(self) self.renderer.add_frame(self.renderer.get_frame()) if stop_condition is not None and stop_condition(): time_progression.close() @@ -817,7 +820,7 @@ def wait_internal(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): # Do nothing return self else: - self.renderer.update_frame() + self.renderer.update_frame(self) dt = 1 / self.renderer.camera.frame_rate self.renderer.add_frame( self.renderer.get_frame(), num_frames=int(duration / dt) diff --git a/tests/control_data/logs_data/BasicSceneLoggingTest.txt b/tests/control_data/logs_data/BasicSceneLoggingTest.txt index 576252e492..edee71e9ed 100644 --- a/tests/control_data/logs_data/BasicSceneLoggingTest.txt +++ b/tests/control_data/logs_data/BasicSceneLoggingTest.txt @@ -6,4 +6,4 @@ {"levelname": "INFO", "module": "scene_file_writer", "message": "Animation 0 : Partial movie file written in <>"} {"levelname": "DEBUG", "module": "scene_file_writer", "message": "Partial movie files to combine (1 files): <>"} {"levelname": "INFO", "module": "scene_file_writer", "message": "\nFile ready at <>\n"} -{"levelname": "INFO", "module": "cairo_renderer", "message": "Rendered SquareToCircle\nPlayed 1 animations"} +{"levelname": "INFO", "module": "scene", "message": "Rendered SquareToCircle\nPlayed 1 animations"} From 200d83cd38fd2e6d7bdcd290272d6c1410d877eb Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 11 Oct 2020 00:55:21 -0700 Subject: [PATCH 21/25] Initialize CairoRenderer without a Scene --- manim/renderer/cairo_renderer.py | 15 +++++++++------ manim/scene/scene.py | 3 ++- manim/scene/scene_file_writer.py | 29 +++++++++++++++-------------- tests/test_renderer.py | 11 ++++------- 4 files changed, 30 insertions(+), 28 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 6da07022bd..e68b315785 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -55,7 +55,7 @@ class CairoRenderer: time: time elapsed since initialisation of scene. """ - def __init__(self, scene, camera_class, **kwargs): + def __init__(self, camera_class, **kwargs): # All of the following are set to EITHER the value passed via kwargs, # OR the value stored in the global config dict at the time of # _instance construction_. Before, they were in the CONFIG dict, which @@ -72,16 +72,19 @@ def __init__(self, scene, camera_class, **kwargs): ]: self.video_quality_config[attr] = kwargs.get(attr, config[attr]) self.camera = camera_class(self.video_quality_config, **camera_config) - self.file_writer = SceneFileWriter( - self.video_quality_config, - scene, - **file_writer_config, - ) self.original_skipping_status = file_writer_config["skip_animations"] self.animations_hashes = [] self.num_plays = 0 self.time = 0 + def init_file_writer(self, scene_name): + self.file_writer = SceneFileWriter( + self, + self.video_quality_config, + scene_name, + **file_writer_config, + ) + @manage_scene_reference @handle_caching_play @handle_play_like_call diff --git a/manim/scene/scene.py b/manim/scene/scene.py index c244c6ebfa..1b6b0a44b5 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -65,7 +65,8 @@ def construct(self): def __init__(self, **kwargs): Container.__init__(self, **kwargs) - self.renderer = CairoRenderer(self, self.camera_class) + self.renderer = CairoRenderer(self.camera_class) + self.renderer.init_file_writer(self.__class__.__name__) self.mobjects = [] # TODO, remove need for foreground mobjects diff --git a/manim/scene/scene_file_writer.py b/manim/scene/scene_file_writer.py index 4e1cc1d397..de80116b85 100644 --- a/manim/scene/scene_file_writer.py +++ b/manim/scene/scene_file_writer.py @@ -42,25 +42,25 @@ class SceneFileWriter(object): """ - def __init__(self, video_quality_config, scene, **kwargs): + def __init__(self, renderer, video_quality_config, scene_name, **kwargs): digest_config(self, kwargs) + self.renderer = renderer self.video_quality_config = video_quality_config - self.scene = scene self.stream_lock = False - self.init_output_directories() + self.init_output_directories(scene_name) self.init_audio() self.frame_count = 0 self.partial_movie_files = [] # Output directories and files - def init_output_directories(self): + def init_output_directories(self, scene_name): """ This method initialises the directories to which video files will be written to and read from (within MEDIA_DIR). If they don't already exist, they will be created. """ module_directory = self.get_default_module_directory() - scene_name = self.get_default_scene_name() + default_name = self.get_default_scene_name(scene_name) if file_writer_config["save_last_frame"] or file_writer_config["save_pngs"]: if file_writer_config["media_dir"] != "": if not file_writer_config["custom_folders"]: @@ -73,7 +73,7 @@ def init_output_directories(self): else: image_dir = guarantee_existence(file_writer_config["images_dir"]) self.image_file_path = os.path.join( - image_dir, add_extension_if_not_present(scene_name, ".png") + image_dir, add_extension_if_not_present(default_name, ".png") ) if file_writer_config["write_to_movie"]: @@ -93,18 +93,19 @@ def init_output_directories(self): self.movie_file_path = os.path.join( movie_dir, add_extension_if_not_present( - scene_name, file_writer_config["movie_file_extension"] + default_name, file_writer_config["movie_file_extension"] ), ) self.gif_file_path = os.path.join( - movie_dir, add_extension_if_not_present(scene_name, GIF_FILE_EXTENSION) + movie_dir, + add_extension_if_not_present(default_name, GIF_FILE_EXTENSION), ) if not file_writer_config["custom_folders"]: self.partial_movie_directory = guarantee_existence( os.path.join( movie_dir, "partial_movie_files", - scene_name, + default_name, ) ) else: @@ -113,7 +114,7 @@ def init_output_directories(self): file_writer_config["media_dir"], "temp_files", "partial_movie_files", - scene_name, + default_name, ) ) @@ -154,7 +155,7 @@ def get_default_module_directory(self): root, _ = os.path.splitext(filename) return root - def get_default_scene_name(self): + def get_default_scene_name(self, scene_name): """ This method returns the default scene name which is the value of "output_file", if it exists and @@ -167,7 +168,7 @@ def get_default_scene_name(self): The default scene name. """ fn = file_writer_config["output_file"] - return fn if fn else self.scene.__class__.__name__ + return fn if fn else scene_name def get_resolution_directory(self): """Get the name of the resolution directory directly containing @@ -400,7 +401,7 @@ def open_movie_pipe(self): FFMPEG and begin writing to FFMPEG's input buffer. """ - file_path = self.partial_movie_files[self.scene.renderer.num_plays] + file_path = self.partial_movie_files[self.renderer.num_plays] # TODO #486 Why does ffmpeg need temp files ? temp_file_path = ( @@ -464,7 +465,7 @@ def close_movie_pipe(self): self.partial_movie_file_path, ) logger.info( - f"Animation {self.scene.renderer.num_plays} : Partial movie file written in %(path)s", + f"Animation {self.renderer.num_plays} : Partial movie file written in %(path)s", {"path": {self.partial_movie_file_path}}, ) diff --git a/tests/test_renderer.py b/tests/test_renderer.py index 6995b0067e..04458b2da0 100644 --- a/tests/test_renderer.py +++ b/tests/test_renderer.py @@ -6,22 +6,19 @@ def test_renderer(): """Test that CairoRenderer instances initialize to the correct config.""" # by default, use the config assert ( - CairoRenderer(None, Camera).video_quality_config["frame_width"] + CairoRenderer(Camera).video_quality_config["frame_width"] == config["frame_width"] ) # init args override config assert ( - CairoRenderer(None, Camera, frame_width=10).video_quality_config["frame_width"] - == 10 + CairoRenderer(Camera, frame_width=10).video_quality_config["frame_width"] == 10 ) # if config changes, reflect those changes with tempconfig({"frame_width": 100}): - assert CairoRenderer(None, Camera).video_quality_config["frame_width"] == 100 + assert CairoRenderer(Camera).video_quality_config["frame_width"] == 100 # ..init args still override new config assert ( - CairoRenderer(None, Camera, frame_width=10).video_quality_config[ - "frame_width" - ] + CairoRenderer(Camera, frame_width=10).video_quality_config["frame_width"] == 10 ) From e7a38a95f7479c7fcd0a6523c46d118dd5c7896d Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 11 Oct 2020 13:21:05 -0700 Subject: [PATCH 22/25] Decouple CairoRenderer from Scene --- manim/__main__.py | 1 + manim/renderer/cairo_renderer.py | 11 +++++++---- manim/scene/scene.py | 9 ++++++--- tests/test_container.py | 2 +- tests/utils/GraphicalUnitTester.py | 3 +-- 5 files changed, 16 insertions(+), 10 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index 1d6fd193a7..c8034558d9 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -19,6 +19,7 @@ from .scene.scene import Scene from .utils.file_ops import open_file as open_media_file from .grpc.impl import frame_server_impl +from .renderer.cairo_renderer import CairoRenderer def open_file_if_needed(file_writer): diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index e68b315785..7f4b9e9044 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -6,6 +6,7 @@ from ..constants import DEFAULT_WAIT_TIME from ..scene.scene_file_writer import SceneFileWriter from ..utils.caching import handle_caching_play, handle_caching_wait +from ..camera.camera import Camera def manage_scene_reference(func): @@ -55,13 +56,14 @@ class CairoRenderer: time: time elapsed since initialisation of scene. """ - def __init__(self, camera_class, **kwargs): + def __init__(self, camera_class=None, **kwargs): # All of the following are set to EITHER the value passed via kwargs, # OR the value stored in the global config dict at the time of # _instance construction_. Before, they were in the CONFIG dict, which # is a class attribute and is defined at the time of _class # definition_. This did not allow for creating two Cameras with # different configurations in the same session. + self.file_writer = None self.video_quality_config = {} for attr in [ "pixel_height", @@ -71,17 +73,18 @@ def __init__(self, camera_class, **kwargs): "frame_rate", ]: self.video_quality_config[attr] = kwargs.get(attr, config[attr]) - self.camera = camera_class(self.video_quality_config, **camera_config) + camera_cls = camera_class if camera_class is not None else Camera + self.camera = camera_cls(self.video_quality_config, **camera_config) self.original_skipping_status = file_writer_config["skip_animations"] self.animations_hashes = [] self.num_plays = 0 self.time = 0 - def init_file_writer(self, scene_name): + def init(self, scene): self.file_writer = SceneFileWriter( self, self.video_quality_config, - scene_name, + scene.__class__.__name__, **file_writer_config, ) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 1b6b0a44b5..8e18f8a5b8 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -63,10 +63,13 @@ def construct(self): "random_seed": 0, } - def __init__(self, **kwargs): + def __init__(self, renderer=None, **kwargs): Container.__init__(self, **kwargs) - self.renderer = CairoRenderer(self.camera_class) - self.renderer.init_file_writer(self.__class__.__name__) + if renderer is None: + self.renderer = CairoRenderer(camera_class=self.camera_class) + else: + self.renderer = renderer + self.renderer.init(self) self.mobjects = [] # TODO, remove need for foreground mobjects diff --git a/tests/test_container.py b/tests/test_container.py index 5a26b3ba56..880b2e5c8f 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -1,5 +1,5 @@ import pytest -from manim import Container, Mobject, Scene +from manim import Container, Mobject, Scene, CairoRenderer def test_ABC(): diff --git a/tests/utils/GraphicalUnitTester.py b/tests/utils/GraphicalUnitTester.py index 698f1a0bb0..5935bf4fd9 100644 --- a/tests/utils/GraphicalUnitTester.py +++ b/tests/utils/GraphicalUnitTester.py @@ -7,7 +7,7 @@ import warnings from platform import system -from manim import config, file_writer_config +from manim import config, file_writer_config, CairoRenderer class GraphicalUnitTester: @@ -75,7 +75,6 @@ def __init__( ]: os.makedirs(dir_temp) - # By invoking this, the scene is rendered. self.scene = scene_object() self.scene.render() From 7286453a92bd92d9d44c323aa7485f9c98c0d03a Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 11 Oct 2020 13:32:44 -0700 Subject: [PATCH 23/25] Pass scene argument to play and wait --- manim/renderer/cairo_renderer.py | 22 ++++++++++------------ manim/utils/caching.py | 20 ++++++++++---------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 7f4b9e9044..2760d54653 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -9,11 +9,9 @@ from ..camera.camera import Camera -def manage_scene_reference(func): +def pass_scene_reference(func): def wrapper(self, scene, *args, **kwargs): - setattr(self, "scene", scene) - func(self, *args, **kwargs) - delattr(self, "scene") + func(self, scene, *args, **kwargs) return wrapper @@ -39,10 +37,10 @@ def handle_play_like_call(func): to the video file stream. """ - def wrapper(self, *args, **kwargs): + def wrapper(self, scene, *args, **kwargs): allow_write = not file_writer_config["skip_animations"] self.file_writer.begin_animation(allow_write) - func(self, *args, **kwargs) + func(self, scene, *args, **kwargs) self.file_writer.end_animation(allow_write) self.num_plays += 1 @@ -88,17 +86,17 @@ def init(self, scene): **file_writer_config, ) - @manage_scene_reference + @pass_scene_reference @handle_caching_play @handle_play_like_call - def play(self, *args, **kwargs): - self.scene.play_internal(*args, **kwargs) + def play(self, scene, *args, **kwargs): + scene.play_internal(*args, **kwargs) - @manage_scene_reference + @pass_scene_reference @handle_caching_wait @handle_play_like_call - def wait(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): - self.scene.wait_internal(duration=duration, stop_condition=stop_condition) + def wait(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None): + scene.wait_internal(duration=duration, stop_condition=stop_condition) def update_frame( # TODO Description in Docstring self, diff --git a/manim/utils/caching.py b/manim/utils/caching.py index e6f745aba0..66cb6521bc 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -15,21 +15,21 @@ def handle_caching_play(func): The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`. """ - def wrapper(self, *args, **kwargs): + def wrapper(self, scene, *args, **kwargs): self.revert_to_original_skipping_status() self.update_skipping_status() - animations = self.scene.compile_play_args_to_animation_list(*args, **kwargs) - self.scene.add_mobjects_from_animations(animations) + animations = scene.compile_play_args_to_animation_list(*args, **kwargs) + scene.add_mobjects_from_animations(animations) if file_writer_config["skip_animations"]: logger.debug(f"Skipping animation {self.num_plays}") - func(self, *args, **kwargs) + func(self, scene, *args, **kwargs) # If the animation is skipped, we mark its hash as None. # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. self.animations_hashes.append(None) self.file_writer.add_partial_movie_file(None) return if not file_writer_config["disable_caching"]: - mobjects_on_scene = self.scene.get_mobjects() + mobjects_on_scene = scene.get_mobjects() hash_play = get_hash_from_play_call( self, self.camera, animations, mobjects_on_scene ) @@ -47,7 +47,7 @@ def wrapper(self, *args, **kwargs): "List of the first few animation hashes of the scene: %(h)s", {"h": str(self.animations_hashes[:5])}, ) - func(self, *args, **kwargs) + func(self, scene, *args, **kwargs) return wrapper @@ -64,12 +64,12 @@ def handle_caching_wait(func): The wait like function that has to be written to the video file stream. Take the same parameters as `scene.wait`. """ - def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): + def wrapper(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None): self.revert_to_original_skipping_status() self.update_skipping_status() if file_writer_config["skip_animations"]: logger.debug(f"Skipping wait {self.num_plays}") - func(self, duration, stop_condition) + func(self, scene, duration, stop_condition) # If the animation is skipped, we mark its hash as None. # When sceneFileWriter will start combining partial movie files, it won't take into account None hashes. self.animations_hashes.append(None) @@ -77,7 +77,7 @@ def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): return if not file_writer_config["disable_caching"]: hash_wait = get_hash_from_wait_call( - self, self.camera, duration, stop_condition, self.scene.get_mobjects() + self, self.camera, duration, stop_condition, scene.get_mobjects() ) if self.file_writer.is_already_cached(hash_wait): logger.info( @@ -92,6 +92,6 @@ def wrapper(self, duration=DEFAULT_WAIT_TIME, stop_condition=None): "Animations hashes list of the scene : (concatened to 5) %(h)s", {"h": str(self.animations_hashes[:5])}, ) - func(self, duration, stop_condition) + func(self, scene, duration, stop_condition) return wrapper From e00f7b33a4fac540c4c210cf65de08620ab852c5 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Sun, 11 Oct 2020 21:59:04 -0700 Subject: [PATCH 24/25] Restore --save_last_frame functionality --- manim/renderer/cairo_renderer.py | 3 ++- manim/scene/scene.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index 2760d54653..a619ce6ccf 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -208,8 +208,9 @@ def revert_to_original_skipping_status(self): file_writer_config["skip_animations"] = self.original_skipping_status return self - def finish(self): + def finish(self, scene): file_writer_config["skip_animations"] = False self.file_writer.finish() if file_writer_config["save_last_frame"]: + self.update_frame(scene, ignore_skipping=True) self.file_writer.save_final_image(self.camera.get_image()) diff --git a/manim/scene/scene.py b/manim/scene/scene.py index 8e18f8a5b8..d35aebbe77 100644 --- a/manim/scene/scene.py +++ b/manim/scene/scene.py @@ -89,7 +89,7 @@ def render(self): except EndSceneEarlyException: pass self.tear_down() - self.renderer.finish() + self.renderer.finish(self) logger.info( f"Rendered {str(self)}\nPlayed {self.renderer.num_plays} animations" ) From 44a0deed794843ef062d73ab5173bf7dc1e4e7a3 Mon Sep 17 00:00:00 2001 From: Devin Neal Date: Wed, 14 Oct 2020 21:13:58 -0700 Subject: [PATCH 25/25] Address review comments --- manim/__main__.py | 9 +-------- manim/renderer/cairo_renderer.py | 3 +-- manim/utils/caching.py | 23 ++++++++++++++--------- manim/utils/family.py | 5 +++-- tests/test_container.py | 2 +- tests/utils/GraphicalUnitTester.py | 9 ++------- 6 files changed, 22 insertions(+), 29 deletions(-) diff --git a/manim/__main__.py b/manim/__main__.py index c8034558d9..2ea9593d43 100644 --- a/manim/__main__.py +++ b/manim/__main__.py @@ -1,14 +1,9 @@ -import inspect import os import platform -import subprocess as sp import sys -import re import traceback -import importlib.util -import types -from . import constants, logger, console, file_writer_config +from . import logger, file_writer_config from .config.config import camera_config, args from .config import cfg_subcmds from .utils.module_ops import ( @@ -16,10 +11,8 @@ get_scene_classes_from_module, get_scenes_to_render, ) -from .scene.scene import Scene from .utils.file_ops import open_file as open_media_file from .grpc.impl import frame_server_impl -from .renderer.cairo_renderer import CairoRenderer def open_file_if_needed(file_writer): diff --git a/manim/renderer/cairo_renderer.py b/manim/renderer/cairo_renderer.py index a619ce6ccf..dc97effbb7 100644 --- a/manim/renderer/cairo_renderer.py +++ b/manim/renderer/cairo_renderer.py @@ -1,8 +1,7 @@ import numpy as np -from .. import config, camera_config, file_writer_config, logger +from .. import config, camera_config, file_writer_config from ..utils.iterables import list_update from ..utils.exceptions import EndSceneEarlyException -from ..utils.hashing import get_hash_from_play_call, get_hash_from_wait_call from ..constants import DEFAULT_WAIT_TIME from ..scene.scene_file_writer import SceneFileWriter from ..utils.caching import handle_caching_play, handle_caching_wait diff --git a/manim/utils/caching.py b/manim/utils/caching.py index 66cb6521bc..6b611b96c3 100644 --- a/manim/utils/caching.py +++ b/manim/utils/caching.py @@ -4,15 +4,18 @@ def handle_caching_play(func): - """ - Decorator that returns a wrapped version of func that will compute the hash of the play invocation. + """Decorator that returns a wrapped version of func that will compute + the hash of the play invocation. - The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. + The returned function will act according to the computed hash: either skip + the animation because it's already cached, or let the invoked function + play normally. Parameters ---------- func : Callable[[...], None] - The play like function that has to be written to the video file stream. Take the same parameters as `scene.play`. + The play like function that has to be written to the video file stream. + Take the same parameters as `scene.play`. """ def wrapper(self, scene, *args, **kwargs): @@ -53,15 +56,17 @@ def wrapper(self, scene, *args, **kwargs): def handle_caching_wait(func): - """ - Decorator that returns a wrapped version of func that will compute the hash of the wait invocation. + """Decorator that returns a wrapped version of func that will compute the hash of + the wait invocation. - The returned function will act according to the computed hash: either skip the animation because it's already cached, or let the invoked function play normally. + The returned function will act according to the computed hash: either skip the + animation because it's already cached, or let the invoked function play normally. Parameters ---------- func : Callable[[...], None] - The wait like function that has to be written to the video file stream. Take the same parameters as `scene.wait`. + The wait like function that has to be written to the video file stream. + Take the same parameters as `scene.wait`. """ def wrapper(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None): @@ -89,7 +94,7 @@ def wrapper(self, scene, duration=DEFAULT_WAIT_TIME, stop_condition=None): self.animations_hashes.append(hash_wait) self.file_writer.add_partial_movie_file(hash_wait) logger.debug( - "Animations hashes list of the scene : (concatened to 5) %(h)s", + "List of the first few animation hashes of the scene: %(h)s", {"h": str(self.animations_hashes[:5])}, ) func(self, scene, duration, stop_condition) diff --git a/manim/utils/family.py b/manim/utils/family.py index b7f8498b93..428f2c816d 100644 --- a/manim/utils/family.py +++ b/manim/utils/family.py @@ -7,8 +7,9 @@ def extract_mobject_family_members( mobjects, use_z_index=False, only_those_with_points=False ): - """Returns a list of the types of mobjects and - their family members present. + """Returns a list of the types of mobjects and their family members present. + A "family" in this context refers to a mobject, its submobjects, and their + submobjects, recursively. Parameters ---------- diff --git a/tests/test_container.py b/tests/test_container.py index 880b2e5c8f..5a26b3ba56 100644 --- a/tests/test_container.py +++ b/tests/test_container.py @@ -1,5 +1,5 @@ import pytest -from manim import Container, Mobject, Scene, CairoRenderer +from manim import Container, Mobject, Scene def test_ABC(): diff --git a/tests/utils/GraphicalUnitTester.py b/tests/utils/GraphicalUnitTester.py index 5935bf4fd9..b4352b7080 100644 --- a/tests/utils/GraphicalUnitTester.py +++ b/tests/utils/GraphicalUnitTester.py @@ -1,13 +1,8 @@ -import numpy as np import os -import sys -import inspect import logging -import pytest -import warnings -from platform import system +import numpy as np -from manim import config, file_writer_config, CairoRenderer +from manim import config, file_writer_config class GraphicalUnitTester: